changeset 493:0b228ee775fe experimental

Copy over trunk.
author jruigrok
date Thu, 15 Apr 2010 05:54:28 +0000
parents 2dcbbdabfb71
children cddcd04db33e
files babel3/COPYING babel3/ChangeLog babel3/INSTALL.txt babel3/MANIFEST.in babel3/README.txt babel3/babel/__init__.py babel3/babel/core.py babel3/babel/dates.py babel3/babel/localedata.py babel3/babel/messages/__init__.py babel3/babel/messages/catalog.py babel3/babel/messages/checkers.py babel3/babel/messages/extract.py babel3/babel/messages/frontend.py babel3/babel/messages/jslexer.py babel3/babel/messages/mofile.py babel3/babel/messages/plurals.py babel3/babel/messages/pofile.py babel3/babel/messages/tests/__init__.py babel3/babel/messages/tests/catalog.py babel3/babel/messages/tests/checkers.py babel3/babel/messages/tests/data/mapping.cfg babel3/babel/messages/tests/data/project/__init__.py babel3/babel/messages/tests/data/project/file1.py babel3/babel/messages/tests/data/project/file2.py babel3/babel/messages/tests/data/project/i18n/de/LC_MESSAGES/messages.mo babel3/babel/messages/tests/data/project/i18n/de/LC_MESSAGES/messages.po babel3/babel/messages/tests/data/project/i18n/de_DE/LC_MESSAGES/messages.po babel3/babel/messages/tests/data/project/i18n/messages.pot babel3/babel/messages/tests/data/project/i18n/messages_non_fuzzy.pot babel3/babel/messages/tests/data/project/i18n/ru_RU/LC_MESSAGES/messages.po babel3/babel/messages/tests/data/project/ignored/a_test_file.txt babel3/babel/messages/tests/data/project/ignored/an_example.txt babel3/babel/messages/tests/data/project/ignored/this_wont_normally_be_here.py babel3/babel/messages/tests/data/setup.cfg babel3/babel/messages/tests/data/setup.py babel3/babel/messages/tests/extract.py babel3/babel/messages/tests/frontend.py babel3/babel/messages/tests/mofile.py babel3/babel/messages/tests/plurals.py babel3/babel/messages/tests/pofile.py babel3/babel/numbers.py babel3/babel/plural.py babel3/babel/support.py babel3/babel/tests/__init__.py babel3/babel/tests/core.py babel3/babel/tests/dates.py babel3/babel/tests/localedata.py babel3/babel/tests/numbers.py babel3/babel/tests/plural.py babel3/babel/tests/support.py babel3/babel/tests/util.py babel3/babel/util.py babel3/contrib/babel.js babel3/doc/cmdline.txt babel3/doc/dates.txt babel3/doc/display.txt babel3/doc/index.txt babel3/doc/intro.txt babel3/doc/logo.pdf babel3/doc/logo.png babel3/doc/logo_small.png babel3/doc/messages.txt babel3/doc/numbers.txt babel3/doc/setup.txt babel3/doc/support.txt babel3/scripts/dump_data.py babel3/scripts/dump_global.py babel3/scripts/import_cldr.py babel3/setup.cfg babel3/setup.py
diffstat 71 files changed, 14244 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/babel3/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.
new file mode 100644
--- /dev/null
+++ b/babel3/ChangeLog
@@ -0,0 +1,163 @@
+Version 1.0
+http://svn.edgewall.org/repos/babel/tags/1.0.0/
+(???, from branches/stable/1.0.x)
+
+ * Added support for the locale plural rules defined by the CLDR.
+ * Added `format_timedelta` function to support localized formatting of
+   relative times with strings such as "2 days" or "1 month" (ticket #126).
+ * Fixed Python 2.3 compatibility (ticket #146).
+ * Fixed negative offset handling of Catalog._set_mime_headers (ticket #165).
+ * Fixed the case where messages containing square brackets would break with
+   an unpack error.
+ * Updated to CLDR 1.7.
+ * Make the CLDR import script work with Python 2.7.
+ * Fix Serbian plural forms (ticket #213).
+
+
+Version 0.9.5
+http://svn.edgewall.org/repos/babel/tags/0.9.5/
+(Apr 6 2010, from branches/stable/0.9.x)
+
+ * Fixed the case where messages containing square brackets would break with
+   an unpack error.
+ * Backport of r467: Fuzzy matching regarding plurals should *NOT* be checked
+   against len(message.id)  because this is always 2, instead, it's should be
+   checked against catalog.num_plurals (ticket #212).
+
+
+Version 0.9.4
+http://svn.edgewall.org/repos/babel/tags/0.9.4/
+(Aug 25 2008, from branches/stable/0.9.x)
+
+ * Currency symbol definitions that is defined with choice patterns in the
+   CLDR data are no longer imported, so the symbol code will be used instead.
+ * Fixed quarter support in date formatting.
+ * Fixed a serious memory leak that was introduces by the support for CLDR
+   aliases in 0.9.3 (ticket #128).
+ * Locale modifiers such as "@euro" are now stripped from locale identifiers
+   when parsing (ticket #136).
+ * The system locales "C" and "POSIX" are now treated as aliases for
+   "en_US_POSIX", for which the CLDR provides the appropriate data. Thanks to
+   Manlio Perillo for the suggestion.
+ * Fixed JavaScript extraction for regular expression literals (ticket #138)
+   and concatenated strings.
+ * The `Translation` class in `babel.support` can now manage catalogs with
+   different message domains, and exposes the family of `d*gettext` functions
+   (ticket #137).
+
+
+Version 0.9.3
+http://svn.edgewall.org/repos/babel/tags/0.9.3/
+(Jul 9 2008, from branches/stable/0.9.x)
+
+ * Fixed invalid message extraction methods causing an UnboundLocalError.
+ * Extraction method specification can now use a dot instead of the colon to
+   separate module and function name (ticket #105).
+ * Fixed message catalog compilation for locales with more than two plural
+   forms (ticket #95).
+ * Fixed compilation of message catalogs for locales with more than two plural
+   forms where the translations were empty (ticket #97).
+ * The stripping of the comment tags in comments is optional now and
+   is done for each line in a comment.
+ * Added a JavaScript message extractor.
+ * Updated to CLDR 1.6.
+ * Fixed timezone calculations when formatting datetime and time values.
+ * Added a `get_plural` function into the plurals module that returns the
+   correct plural forms for a locale as tuple.
+ * Added support for alias definitions in the CLDR data files, meaning that
+   the chance for items missing in certain locales should be greatly reduced
+   (ticket #68).
+
+
+Version 0.9.2
+http://svn.edgewall.org/repos/babel/tags/0.9.2/
+(Feb 4 2008, from branches/stable/0.9.x)
+
+ * Fixed catalogs' charset values not being recognized (ticket #66).
+ * Numerous improvements to the default plural forms.
+ * Fixed fuzzy matching when updating message catalogs (ticket #82).
+ * Fixed bug in catalog updating, that in some cases pulled in translations
+   from different catalogs based on the same template.
+ * Location lines in PO files do no longer get wrapped at hyphens in file
+   names (ticket #79).
+ * Fixed division by zero error in catalog compilation on empty catalogs
+   (ticket #60).
+
+
+Version 0.9.1
+http://svn.edgewall.org/repos/babel/tags/0.9.1/
+(Sep 7 2007, from branches/stable/0.9.x)
+
+ * Fixed catalog updating when a message is merged that was previously simple
+   but now has a plural form, for example by moving from `gettext` to
+   `ngettext`, or vice versa.
+ * Fixed time formatting for 12 am and 12 pm.
+ * Fixed output encoding of the `pybabel --list-locales` command.
+ * MO files are now written in binary mode on windows (ticket #61).
+
+
+Version 0.9
+http://svn.edgewall.org/repos/babel/tags/0.9.0/
+(Aug 20 2007, from branches/stable/0.9.x)
+
+ * The `new_catalog` distutils command has been renamed to `init_catalog` for
+   consistency with the command-line frontend.
+ * Added compilation of message catalogs to MO files (ticket #21).
+ * Added updating of message catalogs from POT files (ticket #22).
+ * Support for significant digits in number formatting.
+ * Apply proper "banker's rounding" in number formatting in a cross-platform
+   manner.
+ * The number formatting functions now also work with numbers represented by
+   Python `Decimal` objects (ticket #53).
+ * Added extensible infrastructure for validating translation catalogs.
+ * Fixed the extractor not filtering out messages that didn't validate against
+   the keyword's specification (ticket #39).
+ * Fixed the extractor raising an exception when encountering an empty string
+   msgid. It now emits a warning to stderr.
+ * Numerous Python message extractor fixes: it now handles nested function
+   calls within a gettext function call correctly, uses the correct line number
+   for multi-line function calls, and other small fixes (tickets #38 and #39).
+ * Improved support for detecting Python string formatting fields in message
+   strings (ticket #57).
+ * CLDR upgraded to the 1.5 release.
+ * Improved timezone formatting.
+ * Implemented scientific number formatting.
+ * Added mechanism to lookup locales by alias, for cases where browsers insist
+   on including only the language code in the `Accept-Language` header, and
+   sometimes even the incorrect language code.
+
+
+Version 0.8.1
+http://svn.edgewall.org/repos/babel/tags/0.8.1/
+(Jul 2 2007, from branches/stable/0.8.x)
+
+ * `default_locale()` would fail when the value of the `LANGUAGE` environment
+   variable contained multiple language codes separated by colon, as is
+   explicitly allowed by the GNU gettext tools. As the `default_locale()`
+   function is called at the module level in some modules, this bug would
+   completely break importing these modules on systems where `LANGUAGE` is set
+   that way.
+ * The character set specified in PO template files is now respected when
+   creating new catalog files based on that template. This allows the use of
+   characters outside the ASCII range in POT files (ticket #17).
+ * The default ordering of messages in generated POT files, which is based on
+   the order those messages are found when walking the source tree, is no
+   longer subject to differences between platforms; directory and file names
+   are now always sorted alphabetically.
+ * The Python message extractor now respects the special encoding comment to be
+   able to handle files containing non-ASCII characters (ticket #23).
+ * Added 'N_' (gettext noop) to the extractor's default keywords.
+ * Made locale string parsing more robust, and also take the script part into
+   account (ticket #27).
+ * Added a function to list all locales for which locale data is available.
+ * Added a command-line option to the `pybabel` command which prints out all
+   available locales (ticket #24).
+ * The name of the command-line script has been changed from just `babel` to
+   `pybabel` to avoid a conflict with the OpenBabel project (ticket #34).
+
+
+Version 0.8
+http://svn.edgewall.org/repos/babel/tags/0.8.0/
+(Jun 20 2007, from branches/stable/0.8.x)
+
+ * First public release
new file mode 100644
--- /dev/null
+++ b/babel3/INSTALL.txt
@@ -0,0 +1,39 @@
+Installing Babel
+================
+
+Prerequisites
+-------------
+
+ * Python 2.3 or later (2.4 or later is recommended)
+ * CLDR 1.7
+ * Optional: setuptools 0.6b1 or later
+ * Optional: pytz (strongly recommended for real time-zone support)
+
+
+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
new file mode 100644
--- /dev/null
+++ b/babel3/MANIFEST.in
@@ -0,0 +1,4 @@
+include babel/global.dat
+include babel/localedata/*.dat
+include doc/api/*.*
+include doc/*.html
new file mode 100644
--- /dev/null
+++ b/babel3/README.txt
@@ -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/>
new file mode 100644
--- /dev/null
+++ b/babel3/babel/__init__.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2008 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 *
+
+__docformat__ = 'restructuredtext en'
+try:
+    from pkg_resources import get_distribution, ResolutionError
+    try:
+        __version__ = get_distribution('Babel').version
+    except ResolutionError:
+        __version__ = None # unknown
+except ImportError:
+    __version__ = None # unknown
new file mode 100644
--- /dev/null
+++ b/babel3/babel/core.py
@@ -0,0 +1,804 @@
+# -*- 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/.
+
+"""Core locale representation and locale data access."""
+
+import os
+import pickle
+
+from babel import localedata
+
+__all__ = ['UnknownLocaleError', 'Locale', 'default_locale', 'negotiate_locale',
+           'parse_locale']
+__docformat__ = 'restructuredtext en'
+
+_global_data = None
+
+def get_global(key):
+    """Return the dictionary for the given key in the global data.
+    
+    The global data is stored in the ``babel/global.dat`` file and contains
+    information independent of individual locales.
+    
+    >>> get_global('zone_aliases')['UTC']
+    'Etc/GMT'
+    >>> get_global('zone_territories')['Europe/Berlin']
+    'DE'
+    
+    :param key: the data key
+    :return: the dictionary found in the global data under the given key
+    :rtype: `dict`
+    :since: version 0.9
+    """
+    global _global_data
+    if _global_data is None:
+        dirname = os.path.join(os.path.dirname(__file__))
+        filename = os.path.join(dirname, 'global.dat')
+        fileobj = open(filename, 'rb')
+        try:
+            _global_data = pickle.load(fileobj)
+        finally:
+            fileobj.close()
+    return _global_data.get(key, {})
+
+
+LOCALE_ALIASES = {
+    'ar': 'ar_SY', 'bg': 'bg_BG', 'bs': 'bs_BA', 'ca': 'ca_ES', 'cs': 'cs_CZ', 
+    'da': 'da_DK', 'de': 'de_DE', 'el': 'el_GR', 'en': 'en_US', 'es': 'es_ES', 
+    'et': 'et_EE', 'fa': 'fa_IR', 'fi': 'fi_FI', 'fr': 'fr_FR', 'gl': 'gl_ES', 
+    'he': 'he_IL', 'hu': 'hu_HU', 'id': 'id_ID', 'is': 'is_IS', 'it': 'it_IT', 
+    'ja': 'ja_JP', 'km': 'km_KH', 'ko': 'ko_KR', 'lt': 'lt_LT', 'lv': 'lv_LV', 
+    'mk': 'mk_MK', 'nl': 'nl_NL', 'nn': 'nn_NO', 'no': 'nb_NO', 'pl': 'pl_PL', 
+    'pt': 'pt_PT', 'ro': 'ro_RO', 'ru': 'ru_RU', 'sk': 'sk_SK', 'sl': 'sl_SI', 
+    'sv': 'sv_SE', 'th': 'th_TH', 'tr': 'tr_TR', 'uk': 'uk_UA'
+}
+
+
+class UnknownLocaleError(Exception):
+    """Exception thrown when a locale is requested for which no locale data
+    is available.
+    """
+
+    def __init__(self, identifier):
+        """Create the exception.
+        
+        :param identifier: the identifier string of the unsupported locale
+        """
+        Exception.__init__(self, 'unknown locale %r' % identifier)
+        self.identifier = identifier
+
+
+class Locale(object):
+    """Representation of a specific locale.
+    
+    >>> locale = Locale('en', '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'.'
+    
+    If a locale is requested for which no locale data is available, an
+    `UnknownLocaleError` is raised:
+    
+    >>> Locale.parse('en_DE')
+    Traceback (most recent call last):
+        ...
+    UnknownLocaleError: unknown locale 'en_DE'
+    
+    :see: `IETF RFC 3066 <http://www.ietf.org/rfc/rfc3066.txt>`_
+    """
+
+    def __init__(self, language, territory=None, script=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 script: the script code
+        :param variant: the variant code
+        :raise `UnknownLocaleError`: if no locale data is available for the
+                                     requested locale
+        """
+        self.language = language
+        self.territory = territory
+        self.script = script
+        self.variant = variant
+        self.__data = None
+
+        identifier = str(self)
+        if not localedata.exists(identifier):
+            raise UnknownLocaleError(identifier)
+
+    def default(cls, category=None, aliases=LOCALE_ALIASES):
+        """Return the system default locale for the specified category.
+        
+        >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']:
+        ...     os.environ[name] = ''
+        >>> os.environ['LANG'] = 'fr_FR.UTF-8'
+        >>> Locale.default('LC_MESSAGES')
+        <Locale "fr_FR">
+
+        :param category: one of the ``LC_XXX`` environment variable names
+        :param aliases: a dictionary of aliases for locale identifiers
+        :return: the value of the variable, or any of the fallbacks
+                 (``LANGUAGE``, ``LC_ALL``, ``LC_CTYPE``, and ``LANG``)
+        :rtype: `Locale`
+        :see: `default_locale`
+        """
+        return cls(default_locale(category, aliases=aliases))
+    default = classmethod(default)
+
+    def negotiate(cls, preferred, available, sep='_', aliases=LOCALE_ALIASES):
+        """Find the best match between available and requested locale strings.
+        
+        >>> Locale.negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT'])
+        <Locale "de_DE">
+        >>> Locale.negotiate(['de_DE', 'en_US'], ['en', 'de'])
+        <Locale "de">
+        >>> Locale.negotiate(['de_DE', 'de'], ['en_US'])
+        
+        You can specify the character used in the locale identifiers to separate
+        the differnet components. This separator is applied to both lists. Also,
+        case is ignored in the comparison:
+        
+        >>> Locale.negotiate(['de-DE', 'de'], ['en-us', 'de-de'], sep='-')
+        <Locale "de_DE">
+        
+        :param preferred: the list of locale identifers preferred by the user
+        :param available: the list of locale identifiers available
+        :param aliases: a dictionary of aliases for locale identifiers
+        :return: the `Locale` object for the best match, or `None` if no match
+                 was found
+        :rtype: `Locale`
+        :see: `negotiate_locale`
+        """
+        identifier = negotiate_locale(preferred, available, sep=sep,
+                                      aliases=aliases)
+        if identifier:
+            return Locale.parse(identifier, sep=sep)
+    negotiate = classmethod(negotiate)
+
+    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
+        :raise `UnknownLocaleError`: if no locale data is available for the
+                                     requested locale
+        :see: `parse_locale`
+        """
+        if isinstance(identifier, basestring):
+            return cls(*parse_locale(identifier, sep=sep))
+        return identifier
+    parse = classmethod(parse)
+
+    def __eq__(self, other):
+        return str(self) == str(other)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __repr__(self):
+        return '<Locale "%s">' % str(self)
+
+    def __str__(self):
+        return '_'.join(filter(None, [self.language, self.script,
+                                      self.territory, self.variant]))
+
+    def _data(self):
+        if self.__data is None:
+            self.__data = localedata.LocaleDataDict(localedata.load(str(self)))
+        return self.__data
+    _data = property(_data)
+
+    def get_display_name(self, locale=None):
+        """Return the display name of the locale using the given locale.
+        
+        The display name will include the language, territory, script, and
+        variant, if those are specified.
+        
+        >>> Locale('zh', 'CN', script='Hans').get_display_name('en')
+        u'Chinese (Simplified Han, China)'
+        
+        :param locale: the locale to use
+        :return: the display name
+        """
+        if locale is None:
+            locale = self
+        locale = Locale.parse(locale)
+        retval = locale.languages.get(self.language)
+        if self.territory or self.script or self.variant:
+            details = []
+            if self.script:
+                details.append(locale.scripts.get(self.script))
+            if self.territory:
+                details.append(locale.territories.get(self.territory))
+            if self.variant:
+                details.append(locale.variants.get(self.variant))
+            details = filter(None, details)
+            if details:
+                retval += ' (%s)' % u', '.join(details)
+        return retval
+
+    display_name = property(get_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)'
+        >>> Locale('sv').display_name
+        u'svenska'
+        
+        :type: `unicode`
+        """)
+
+    def english_name(self):
+        return self.get_display_name(Locale('en'))
+    english_name = property(english_name, doc="""\
+        The english display name of the locale.
+        
+        >>> Locale('de').english_name
+        u'German'
+        >>> Locale('de', 'DE').english_name
+        u'German (Germany)'
+        
+        :type: `unicode`
+        """)
+
+    #{ General Locale Display Names
+
+    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`
+        """)
+
+    #{ Number Formatting
+
+    def currencies(self):
+        return self._data['currency_names']
+    currencies = property(currencies, doc="""\
+        Mapping of currency codes to translated currency names.
+        
+        >>> Locale('en').currencies['COP']
+        u'Colombian Peso'
+        >>> Locale('de', 'DE').currencies['COP']
+        u'Kolumbianischer Peso'
+        
+        :type: `dict`
+        """)
+
+    def currency_symbols(self):
+        return self._data['currency_symbols']
+    currency_symbols = property(currency_symbols, doc="""\
+        Mapping of currency codes to symbols.
+        
+        >>> Locale('en', 'US').currency_symbols['USD']
+        u'$'
+        >>> Locale('es', 'CO').currency_symbols['USD']
+        u'US$'
+        
+        :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 decimal_formats(self):
+        return self._data['decimal_formats']
+    decimal_formats = property(decimal_formats, doc="""\
+        Locale patterns for decimal number formatting.
+        
+        >>> Locale('en', 'US').decimal_formats[None]
+        <NumberPattern u'#,##0.###'>
+        
+        :type: `dict`
+        """)
+
+    def currency_formats(self):
+        return self._data['currency_formats']
+    currency_formats = property(currency_formats, doc=r"""\
+        Locale patterns for currency number formatting.
+        
+        >>> print Locale('en', 'US').currency_formats[None]
+        <NumberPattern u'\xa4#,##0.00'>
+        
+        :type: `dict`
+        """)
+
+    def percent_formats(self):
+        return self._data['percent_formats']
+    percent_formats = property(percent_formats, doc="""\
+        Locale patterns for percent number formatting.
+        
+        >>> Locale('en', 'US').percent_formats[None]
+        <NumberPattern u'#,##0%'>
+        
+        :type: `dict`
+        """)
+
+    def scientific_formats(self):
+        return self._data['scientific_formats']
+    scientific_formats = property(scientific_formats, doc="""\
+        Locale patterns for scientific number formatting.
+        
+        >>> Locale('en', 'US').scientific_formats[None]
+        <NumberPattern u'#E0'>
+        
+        :type: `dict`
+        """)
+
+    #{ Calendar Information and Date Formatting
+
+    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'][3]
+        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 time_zones(self):
+        return self._data['time_zones']
+    time_zones = property(time_zones, doc="""\
+        Locale display names for time zones.
+        
+        >>> Locale('en', 'US').time_zones['Europe/London']['long']['daylight']
+        u'British Summer Time'
+        >>> Locale('en', 'US').time_zones['America/St_Johns']['city']
+        u"St. John's"
+        
+        :type: `dict`
+        """)
+
+    def meta_zones(self):
+        return self._data['meta_zones']
+    meta_zones = property(meta_zones, doc="""\
+        Locale display names for meta time zones.
+        
+        Meta time zones are basically groups of different Olson time zones that
+        have the same GMT offset and daylight savings time.
+        
+        >>> Locale('en', 'US').meta_zones['Europe_Central']['long']['daylight']
+        u'Central European Summer Time'
+        
+        :type: `dict`
+        :since: version 0.9
+        """)
+
+    def zone_formats(self):
+        return self._data['zone_formats']
+    zone_formats = property(zone_formats, doc=r"""\
+        Patterns related to the formatting of time zones.
+        
+        >>> Locale('en', 'US').zone_formats['fallback']
+        u'%(1)s (%(0)s)'
+        >>> Locale('pt', 'BR').zone_formats['region']
+        u'Hor\xe1rio %s'
+        
+        :type: `dict`
+        :since: version 0.9
+        """)
+
+    def first_week_day(self):
+        return self._data['week_data']['first_day']
+    first_week_day = property(first_week_day, doc="""\
+        The first day of a week, with 0 being Monday.
+        
+        >>> Locale('de', 'DE').first_week_day
+        0
+        >>> Locale('en', 'US').first_week_day
+        6
+        
+        :type: `int`
+        """)
+
+    def weekend_start(self):
+        return self._data['week_data']['weekend_start']
+    weekend_start = property(weekend_start, doc="""\
+        The day the weekend starts, with 0 being Monday.
+        
+        >>> Locale('de', 'DE').weekend_start
+        5
+        
+        :type: `int`
+        """)
+
+    def weekend_end(self):
+        return self._data['week_data']['weekend_end']
+    weekend_end = property(weekend_end, doc="""\
+        The day the weekend ends, with 0 being Monday.
+        
+        >>> Locale('de', 'DE').weekend_end
+        6
+        
+        :type: `int`
+        """)
+
+    def min_week_days(self):
+        return self._data['week_data']['min_days']
+    min_week_days = property(min_week_days, doc="""\
+        The minimum number of days in a week so that the week is counted as the
+        first week of a year or month.
+        
+        >>> Locale('de', 'DE').min_week_days
+        4
+        
+        :type: `int`
+        """)
+
+    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']
+        <DateTimePattern u'M/d/yy'>
+        >>> Locale('fr', 'FR').date_formats['long']
+        <DateTimePattern u'd MMMM y'>
+        
+        :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']
+        <DateTimePattern u'h:mm a'>
+        >>> Locale('fr', 'FR').time_formats['long']
+        <DateTimePattern u'HH:mm:ss z'>
+        
+        :type: `dict`
+        """)
+
+    def datetime_formats(self):
+        return self._data['datetime_formats']
+    datetime_formats = property(datetime_formats, doc="""\
+        Locale patterns for datetime formatting.
+        
+        >>> Locale('en').datetime_formats['full']
+        u'{1} {0}'
+        >>> Locale('th').datetime_formats['medium']
+        u'{1}, {0}'
+        
+        :type: `dict`
+        """)
+
+    def plural_form(self):
+        return self._data['plural_form']
+    plural_form = property(plural_form, doc="""\
+        Plural rules for the locale.
+        
+        >>> Locale('en').plural_form(1)
+        'one'
+        >>> Locale('en').plural_form(0)
+        'other'
+        >>> Locale('fr').plural_form(0)
+        'one'
+        >>> Locale('ru').plural_form(100)
+        'many'
+        
+        :type: `PluralRule`
+        """)
+
+
+def default_locale(category=None, aliases=LOCALE_ALIASES):
+    """Returns the system default locale for a given category, based on
+    environment variables.
+    
+    >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']:
+    ...     os.environ[name] = ''
+    >>> os.environ['LANG'] = 'fr_FR.UTF-8'
+    >>> default_locale('LC_MESSAGES')
+    'fr_FR'
+
+    The "C" or "POSIX" pseudo-locales are treated as aliases for the
+    "en_US_POSIX" locale:
+
+    >>> os.environ['LC_MESSAGES'] = 'POSIX'
+    >>> default_locale('LC_MESSAGES')
+    'en_US_POSIX'
+
+    :param category: one of the ``LC_XXX`` environment variable names
+    :param aliases: a dictionary of aliases for locale identifiers
+    :return: the value of the variable, or any of the fallbacks (``LANGUAGE``,
+             ``LC_ALL``, ``LC_CTYPE``, and ``LANG``)
+    :rtype: `str`
+    """
+    varnames = (category, 'LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG')
+    for name in filter(None, varnames):
+        locale = os.getenv(name)
+        if locale:
+            if name == 'LANGUAGE' and ':' in locale:
+                # the LANGUAGE variable may contain a colon-separated list of
+                # language codes; we just pick the language on the list
+                locale = locale.split(':')[0]
+            if locale in ('C', 'POSIX'):
+                locale = 'en_US_POSIX'
+            elif aliases and locale in aliases:
+                locale = aliases[locale]
+            return '_'.join(filter(None, parse_locale(locale)))
+
+def negotiate_locale(preferred, available, sep='_', aliases=LOCALE_ALIASES):
+    """Find the best match between available and requested locale strings.
+    
+    >>> negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT'])
+    'de_DE'
+    >>> negotiate_locale(['de_DE', 'en_US'], ['en', 'de'])
+    'de'
+    
+    Case is ignored by the algorithm, the result uses the case of the preferred
+    locale identifier:
+    
+    >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at'])
+    'de_DE'
+    
+    >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at'])
+    'de_DE'
+    
+    By default, some web browsers unfortunately do not include the territory
+    in the locale identifier for many locales, and some don't even allow the
+    user to easily add the territory. So while you may prefer using qualified
+    locale identifiers in your web-application, they would not normally match
+    the language-only locale sent by such browsers. To workaround that, this
+    function uses a default mapping of commonly used langauge-only locale
+    identifiers to identifiers including the territory:
+    
+    >>> negotiate_locale(['ja', 'en_US'], ['ja_JP', 'en_US'])
+    'ja_JP'
+    
+    Some browsers even use an incorrect or outdated language code, such as "no"
+    for Norwegian, where the correct locale identifier would actually be "nb_NO"
+    (Bokmål) or "nn_NO" (Nynorsk). The aliases are intended to take care of
+    such cases, too:
+    
+    >>> negotiate_locale(['no', 'sv'], ['nb_NO', 'sv_SE'])
+    'nb_NO'
+    
+    You can override this default mapping by passing a different `aliases`
+    dictionary to this function, or you can bypass the behavior althogher by
+    setting the `aliases` parameter to `None`.
+    
+    :param preferred: the list of locale strings preferred by the user
+    :param available: the list of locale strings available
+    :param sep: character that separates the different parts of the locale
+                strings
+    :param aliases: a dictionary of aliases for locale identifiers
+    :return: the locale identifier for the best match, or `None` if no match
+             was found
+    :rtype: `str`
+    """
+    available = [a.lower() for a in available if a]
+    for locale in preferred:
+        ll = locale.lower()
+        if ll in available:
+            return locale
+        if aliases:
+            alias = aliases.get(ll)
+            if alias:
+                alias = alias.replace('_', sep)
+                if alias.lower() in available:
+                    return alias
+        parts = locale.split(sep)
+        if len(parts) > 1 and parts[0].lower() in available:
+            return parts[0]
+    return None
+
+def parse_locale(identifier, sep='_'):
+    """Parse a locale identifier into a tuple of the form::
+    
+      ``(language, territory, script, variant)``
+    
+    >>> parse_locale('zh_CN')
+    ('zh', 'CN', None, None)
+    >>> parse_locale('zh_Hans_CN')
+    ('zh', 'CN', 'Hans', None)
+    
+    The default component separator is "_", but a different separator can be
+    specified using the `sep` parameter:
+    
+    >>> parse_locale('zh-CN', sep='-')
+    ('zh', 'CN', None, None)
+    
+    If the identifier cannot be parsed into a locale, a `ValueError` exception
+    is raised:
+    
+    >>> parse_locale('not_a_LOCALE_String')
+    Traceback (most recent call last):
+      ...
+    ValueError: 'not_a_LOCALE_String' is not a valid locale identifier
+    
+    Encoding information and locale modifiers are removed from the identifier:
+    
+    >>> parse_locale('it_IT@euro')
+    ('it', 'IT', None, None)
+    >>> parse_locale('en_US.UTF-8')
+    ('en', 'US', None, None)
+    >>> parse_locale('de_DE.iso885915@euro')
+    ('de', 'DE', None, None)
+    
+    :param identifier: the locale identifier string
+    :param sep: character that separates the different components of the locale
+                identifier
+    :return: the ``(language, territory, script, variant)`` tuple
+    :rtype: `tuple`
+    :raise `ValueError`: if the string does not appear to be a valid locale
+                         identifier
+    
+    :see: `IETF RFC 4646 <http://www.ietf.org/rfc/rfc4646.txt>`_
+    """
+    if '.' in identifier:
+        # this is probably the charset/encoding, which we don't care about
+        identifier = identifier.split('.', 1)[0]
+    if '@' in identifier:
+        # this is a locale modifier such as @euro, which we don't care about
+        # either
+        identifier = identifier.split('@', 1)[0]
+
+    parts = identifier.split(sep)
+    lang = parts.pop(0).lower()
+    if not lang.isalpha():
+        raise ValueError('expected only letters, got %r' % lang)
+
+    script = territory = variant = None
+    if parts:
+        if len(parts[0]) == 4 and parts[0].isalpha():
+            script = parts.pop(0).title()
+
+    if parts:
+        if len(parts[0]) == 2 and parts[0].isalpha():
+            territory = parts.pop(0).upper()
+        elif len(parts[0]) == 3 and parts[0].isdigit():
+            territory = parts.pop(0)
+
+    if parts:
+        if len(parts[0]) == 4 and parts[0][0].isdigit() or \
+                len(parts[0]) >= 5 and parts[0][0].isalpha():
+            variant = parts.pop()
+
+    if parts:
+        raise ValueError('%r is not a valid locale identifier' % identifier)
+
+    return lang, territory, script, variant
new file mode 100644
--- /dev/null
+++ b/babel3/babel/dates.py
@@ -0,0 +1,1055 @@
+# -*- 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 __future__ import division
+from datetime import date, datetime, time, timedelta, tzinfo
+import re
+
+from babel.core import default_locale, get_global, Locale
+from babel.util import UTC
+
+__all__ = ['format_date', 'format_datetime', 'format_time', 'format_timedelta',
+           'get_timezone_name', 'parse_date', 'parse_datetime', 'parse_time']
+__docformat__ = 'restructuredtext en'
+
+LC_TIME = default_locale('LC_TIME')
+
+# Aliases for use in scopes where the modules are shadowed by local variables
+date_ = date
+datetime_ = datetime
+time_ = 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'Tuesday'
+    >>> get_day_names('abbreviated', locale='es')[1]
+    u'mar'
+    >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1]
+    u'D'
+    
+    :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", "abbreviated", or "narrow"
+    :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')
+    <DateTimePattern u'MMM d, y'>
+    >>> get_date_format('full', locale='de_DE')
+    <DateTimePattern u'EEEE, d. MMMM y'>
+    
+    :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: `DateTimePattern`
+    """
+    return Locale.parse(locale).date_formats[format]
+
+def get_datetime_format(format='medium', locale=LC_TIME):
+    """Return the datetime formatting patterns used by the locale for the
+    specified format.
+    
+    >>> get_datetime_format(locale='en_US')
+    u'{1} {0}'
+    
+    :param format: the format to use, one of "full", "long", "medium", or
+                   "short"
+    :param locale: the `Locale` object, or a locale string
+    :return: the datetime format pattern
+    :rtype: `unicode`
+    """
+    patterns = Locale.parse(locale).datetime_formats
+    if format not in patterns:
+        format = None
+    return patterns[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')
+    <DateTimePattern u'h:mm:ss a'>
+    >>> get_time_format('full', locale='de_DE')
+    <DateTimePattern u'HH:mm:ss zzzz'>
+    
+    :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: `DateTimePattern`
+    """
+    return Locale.parse(locale).time_formats[format]
+
+def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME):
+    """Return the timezone associated with the given `datetime` object formatted
+    as string indicating the offset from GMT.
+    
+    >>> dt = datetime(2007, 4, 1, 15, 30)
+    >>> get_timezone_gmt(dt, locale='en')
+    u'GMT+00:00'
+    
+    >>> from pytz import timezone
+    >>> tz = timezone('America/Los_Angeles')
+    >>> dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz)
+    >>> get_timezone_gmt(dt, locale='en')
+    u'GMT-08:00'
+    >>> get_timezone_gmt(dt, 'short', locale='en')
+    u'-0800'
+    
+    The long format depends on the locale, for example in France the acronym
+    UTC string is used instead of GMT:
+    
+    >>> get_timezone_gmt(dt, 'long', locale='fr_FR')
+    u'UTC-08:00'
+    
+    :param datetime: the ``datetime`` object; if `None`, the current date and
+                     time in UTC is used
+    :param width: either "long" or "short"
+    :param locale: the `Locale` object, or a locale string
+    :return: the GMT offset representation of the timezone
+    :rtype: `unicode`
+    :since: version 0.9
+    """
+    if datetime is None:
+        datetime = datetime_.utcnow()
+    elif isinstance(datetime, (int, long)):
+        datetime = datetime_.utcfromtimestamp(datetime).time()
+    if datetime.tzinfo is None:
+        datetime = datetime.replace(tzinfo=UTC)
+    locale = Locale.parse(locale)
+
+    offset = datetime.tzinfo.utcoffset(datetime)
+    seconds = offset.days * 24 * 60 * 60 + offset.seconds
+    hours, seconds = divmod(seconds, 3600)
+    if width == 'short':
+        pattern = u'%+03d%02d'
+    else:
+        pattern = locale.zone_formats['gmt'] % '%+03d:%02d'
+    return pattern % (hours, seconds // 60)
+
+def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME):
+    """Return a representation of the given timezone using "location format".
+    
+    The result depends on both the local display name of the country and the
+    city assocaited with the time zone:
+    
+    >>> from pytz import timezone
+    >>> tz = timezone('America/St_Johns')
+    >>> get_timezone_location(tz, locale='de_DE')
+    u"Kanada (St. John's)"
+    >>> tz = timezone('America/Mexico_City')
+    >>> get_timezone_location(tz, locale='de_DE')
+    u'Mexiko (Mexiko-Stadt)'
+    
+    If the timezone is associated with a country that uses only a single
+    timezone, just the localized country name is returned:
+    
+    >>> tz = timezone('Europe/Berlin')
+    >>> get_timezone_name(tz, locale='de_DE')
+    u'Deutschland'
+    
+    :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines
+                         the timezone; if `None`, the current date and time in
+                         UTC is assumed
+    :param locale: the `Locale` object, or a locale string
+    :return: the localized timezone name using location format
+    :rtype: `unicode`
+    :since: version 0.9
+    """
+    if dt_or_tzinfo is None or isinstance(dt_or_tzinfo, (int, long)):
+        dt = None
+        tzinfo = UTC
+    elif isinstance(dt_or_tzinfo, (datetime, time)):
+        dt = dt_or_tzinfo
+        if dt.tzinfo is not None:
+            tzinfo = dt.tzinfo
+        else:
+            tzinfo = UTC
+    else:
+        dt = None
+        tzinfo = dt_or_tzinfo
+    locale = Locale.parse(locale)
+
+    if hasattr(tzinfo, 'zone'):
+        zone = tzinfo.zone
+    else:
+        zone = tzinfo.tzname(dt or datetime.utcnow())
+
+    # Get the canonical time-zone code
+    zone = get_global('zone_aliases').get(zone, zone)
+
+    info = locale.time_zones.get(zone, {})
+
+    # Otherwise, if there is only one timezone for the country, return the
+    # localized country name
+    region_format = locale.zone_formats['region']
+    territory = get_global('zone_territories').get(zone)
+    if territory not in locale.territories:
+        territory = 'ZZ' # invalid/unknown
+    territory_name = locale.territories[territory]
+    if territory and len(get_global('territory_zones').get(territory, [])) == 1:
+        return region_format % (territory_name)
+
+    # Otherwise, include the city in the output
+    fallback_format = locale.zone_formats['fallback']
+    if 'city' in info:
+        city_name = info['city']
+    else:
+        metazone = get_global('meta_zones').get(zone)
+        metazone_info = locale.meta_zones.get(metazone, {})
+        if 'city' in metazone_info:
+            city_name = metainfo['city']
+        elif '/' in zone:
+            city_name = zone.split('/', 1)[1].replace('_', ' ')
+        else:
+            city_name = zone.replace('_', ' ')
+
+    return region_format % (fallback_format % {
+        '0': city_name,
+        '1': territory_name
+    })
+
+def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False,
+                      locale=LC_TIME):
+    r"""Return the localized display name for the given timezone. The timezone
+    may be specified using a ``datetime`` or `tzinfo` object.
+    
+    >>> from pytz import timezone
+    >>> dt = time(15, 30, tzinfo=timezone('America/Los_Angeles'))
+    >>> get_timezone_name(dt, locale='en_US')
+    u'Pacific Standard Time'
+    >>> get_timezone_name(dt, width='short', locale='en_US')
+    u'PST'
+    
+    If this function gets passed only a `tzinfo` object and no concrete
+    `datetime`,  the returned display name is indenpendent of daylight savings
+    time. This can be used for example for selecting timezones, or to set the
+    time of events that recur across DST changes:
+    
+    >>> tz = timezone('America/Los_Angeles')
+    >>> get_timezone_name(tz, locale='en_US')
+    u'Pacific Time'
+    >>> get_timezone_name(tz, 'short', locale='en_US')
+    u'PT'
+    
+    If no localized display name for the timezone is available, and the timezone
+    is associated with a country that uses only a single timezone, the name of
+    that country is returned, formatted according to the locale:
+    
+    >>> tz = timezone('Europe/Berlin')
+    >>> get_timezone_name(tz, locale='de_DE')
+    u'Deutschland'
+    >>> get_timezone_name(tz, locale='pt_BR')
+    u'Hor\xe1rio Alemanha'
+    
+    On the other hand, if the country uses multiple timezones, the city is also
+    included in the representation:
+    
+    >>> tz = timezone('America/St_Johns')
+    >>> get_timezone_name(tz, locale='de_DE')
+    u"Kanada (St. John's)"
+    
+    The `uncommon` parameter can be set to `True` to enable the use of timezone
+    representations that are not commonly used by the requested locale. For
+    example, while in French the central European timezone is usually
+    abbreviated as "HEC", in Canadian French, this abbreviation is not in
+    common use, so a generic name would be chosen by default:
+    
+    >>> tz = timezone('Europe/Paris')
+    >>> get_timezone_name(tz, 'short', locale='fr_CA')
+    u'France'
+    >>> get_timezone_name(tz, 'short', uncommon=True, locale='fr_CA')
+    u'HEC'
+    
+    :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines
+                         the timezone; if a ``tzinfo`` object is used, the
+                         resulting display name will be generic, i.e.
+                         independent of daylight savings time; if `None`, the
+                         current date in UTC is assumed
+    :param width: either "long" or "short"
+    :param uncommon: whether even uncommon timezone abbreviations should be used
+    :param locale: the `Locale` object, or a locale string
+    :return: the timezone display name
+    :rtype: `unicode`
+    :since: version 0.9
+    :see:  `LDML Appendix J: Time Zone Display Names
+            <http://www.unicode.org/reports/tr35/#Time_Zone_Fallback>`_
+    """
+    if dt_or_tzinfo is None or isinstance(dt_or_tzinfo, (int, long)):
+        dt = None
+        tzinfo = UTC
+    elif isinstance(dt_or_tzinfo, (datetime, time)):
+        dt = dt_or_tzinfo
+        if dt.tzinfo is not None:
+            tzinfo = dt.tzinfo
+        else:
+            tzinfo = UTC
+    else:
+        dt = None
+        tzinfo = dt_or_tzinfo
+    locale = Locale.parse(locale)
+
+    if hasattr(tzinfo, 'zone'):
+        zone = tzinfo.zone
+    else:
+        zone = tzinfo.tzname(dt)
+
+    # Get the canonical time-zone code
+    zone = get_global('zone_aliases').get(zone, zone)
+
+    info = locale.time_zones.get(zone, {})
+    # Try explicitly translated zone names first
+    if width in info:
+        if dt is None:
+            field = 'generic'
+        else:
+            dst = tzinfo.dst(dt)
+            if dst is None:
+                field = 'generic'
+            elif dst == 0:
+                field = 'standard'
+            else:
+                field = 'daylight'
+        if field in info[width]:
+            return info[width][field]
+
+    metazone = get_global('meta_zones').get(zone)
+    if metazone:
+        metazone_info = locale.meta_zones.get(metazone, {})
+        if width in metazone_info and (uncommon or metazone_info.get('common')):
+            if dt is None:
+                field = 'generic'
+            else:
+                field = tzinfo.dst(dt) and 'daylight' or 'standard'
+            if field in metazone_info[width]:
+                return metazone_info[width][field]
+
+    # If we have a concrete datetime, we assume that the result can't be
+    # independent of daylight savings time, so we return the GMT offset
+    if dt is not None:
+        return get_timezone_gmt(dt, width=width, locale=locale)
+
+    return get_timezone_location(dt_or_tzinfo, locale=locale)
+
+def format_date(date=None, format='medium', locale=LC_TIME):
+    """Return 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'
+    
+    If you don't want to use the locale default formats, you can specify a
+    custom date pattern:
+    
+    >>> format_date(d, "EEE, MMM d, ''yy", locale='en')
+    u"Sun, Apr 1, '07"
+    
+    :param date: the ``date`` or ``datetime`` object; if `None`, the current
+                 date is used
+    :param format: one of "full", "long", "medium", or "short", or a custom
+                   date/time pattern
+    :param locale: a `Locale` object or a locale identifier
+    :rtype: `unicode`
+    
+    :note: If the pattern contains time fields, an `AttributeError` will be
+           raised when trying to apply the formatting. This is also true if
+           the value of ``date`` parameter is actually a ``datetime`` object,
+           as this function automatically converts that to a ``date``.
+    """
+    if date is None:
+        date = date_.today()
+    elif isinstance(date, datetime):
+        date = date.date()
+
+    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=None, format='medium', tzinfo=None,
+                    locale=LC_TIME):
+    r"""Return a date formatted according to the given pattern.
+    
+    >>> dt = datetime(2007, 04, 01, 15, 30)
+    >>> format_datetime(dt, locale='en_US')
+    u'Apr 1, 2007 3:30:00 PM'
+    
+    For any pattern requiring the display of the time-zone, the third-party
+    ``pytz`` package is needed to explicitly specify the time-zone:
+    
+    >>> from pytz import timezone
+    >>> format_datetime(dt, 'full', tzinfo=timezone('Europe/Paris'),
+    ...                 locale='fr_FR')
+    u'dimanche 1 avril 2007 17:30:00 Heure avanc\xe9e de l\u2019Europe centrale'
+    >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz",
+    ...                 tzinfo=timezone('US/Eastern'), locale='en')
+    u'2007.04.01 AD at 11:30:00 EDT'
+    
+    :param datetime: the `datetime` object; if `None`, the current date and
+                     time is used
+    :param format: one of "full", "long", "medium", or "short", or a custom
+                   date/time pattern
+    :param tzinfo: the timezone to apply to the time for display
+    :param locale: a `Locale` object or a locale identifier
+    :rtype: `unicode`
+    """
+    if datetime is None:
+        datetime = datetime_.utcnow()
+    elif isinstance(datetime, (int, long)):
+        datetime = datetime_.utcfromtimestamp(datetime)
+    elif isinstance(datetime, time):
+        datetime = datetime_.combine(date.today(), datetime)
+    if datetime.tzinfo is None:
+        datetime = datetime.replace(tzinfo=UTC)
+    if tzinfo is not None:
+        datetime = datetime.astimezone(tzinfo)
+        if hasattr(tzinfo, 'normalize'): # pytz
+            datetime = tzinfo.normalize(datetime)
+
+    locale = Locale.parse(locale)
+    if format in ('full', 'long', 'medium', 'short'):
+        return get_datetime_format(format, locale=locale) \
+            .replace('{0}', format_time(datetime, format, tzinfo=None,
+                                        locale=locale)) \
+            .replace('{1}', format_date(datetime, format, locale=locale))
+    else:
+        return parse_pattern(format).apply(datetime, locale)
+
+def format_time(time=None, format='medium', tzinfo=None, locale=LC_TIME):
+    r"""Return 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'
+    
+    If you don't want to use the locale default formats, you can specify a
+    custom time pattern:
+    
+    >>> format_time(t, "hh 'o''clock' a", locale='en')
+    u"03 o'clock PM"
+    
+    For any pattern requiring the display of the time-zone, the third-party
+    ``pytz`` package is needed to explicitly specify the time-zone:
+    
+    >>> from pytz import timezone
+    >>> t = datetime(2007, 4, 1, 15, 30)
+    >>> tzinfo = timezone('Europe/Paris')
+    >>> t = tzinfo.localize(t)
+    >>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR')
+    u'15:30:00 Heure avanc\xe9e de l\u2019Europe centrale'
+    >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=timezone('US/Eastern'),
+    ...             locale='en')
+    u"09 o'clock AM, Eastern Daylight Time"
+    
+    As that example shows, when this function gets passed a
+    ``datetime.datetime`` value, the actual time in the formatted string is
+    adjusted to the timezone specified by the `tzinfo` parameter. If the
+    ``datetime`` is "naive" (i.e. it has no associated timezone information),
+    it is assumed to be in UTC.
+    
+    These timezone calculations are **not** performed if the value is of type
+    ``datetime.time``, as without date information there's no way to determine
+    what a given time would translate to in a different timezone without
+    information about whether daylight savings time is in effect or not. This
+    means that time values are left as-is, and the value of the `tzinfo`
+    parameter is only used to display the timezone name if needed:
+    
+    >>> t = time(15, 30)
+    >>> format_time(t, format='full', tzinfo=timezone('Europe/Paris'),
+    ...             locale='fr_FR')
+    u'15:30:00 Heure normale de l\u2019Europe centrale'
+    >>> format_time(t, format='full', tzinfo=timezone('US/Eastern'),
+    ...             locale='en_US')
+    u'3:30:00 PM Eastern Standard Time'
+    
+    :param time: the ``time`` or ``datetime`` object; if `None`, the current
+                 time in UTC is used
+    :param format: one of "full", "long", "medium", or "short", or a custom
+                   date/time pattern
+    :param tzinfo: the time-zone to apply to the time for display
+    :param locale: a `Locale` object or a locale identifier
+    :rtype: `unicode`
+    
+    :note: If the pattern contains date fields, an `AttributeError` will be
+           raised when trying to apply the formatting. This is also true if
+           the value of ``time`` parameter is actually a ``datetime`` object,
+           as this function automatically converts that to a ``time``.
+    """
+    if time is None:
+        time = datetime.utcnow()
+    elif isinstance(time, (int, long)):
+        time = datetime.utcfromtimestamp(time)
+    if time.tzinfo is None:
+        time = time.replace(tzinfo=UTC)
+    if isinstance(time, datetime):
+        if tzinfo is not None:
+            time = time.astimezone(tzinfo)
+            if hasattr(tzinfo, 'normalize'): # pytz
+                time = tzinfo.normalize(time)
+        time = time.timetz()
+    elif tzinfo is not None:
+        time = time.replace(tzinfo=tzinfo)
+
+    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)
+
+TIMEDELTA_UNITS = (
+    ('year',   3600 * 24 * 365),
+    ('month',  3600 * 24 * 30),
+    ('week',   3600 * 24 * 7),
+    ('day',    3600 * 24),
+    ('hour',   3600),
+    ('minute', 60),
+    ('second', 1)
+)
+
+def format_timedelta(delta, granularity='second', threshold=.85, locale=LC_TIME):
+    """Return a time delta according to the rules of the given locale.
+
+    >>> format_timedelta(timedelta(weeks=12), locale='en_US')
+    u'3 mths'
+    >>> format_timedelta(timedelta(seconds=1), locale='es')
+    u'1 s'
+
+    The granularity parameter can be provided to alter the lowest unit
+    presented, which defaults to a second.
+    
+    >>> format_timedelta(timedelta(hours=3), granularity='day',
+    ...                  locale='en_US')
+    u'1 day'
+
+    The threshold parameter can be used to determine at which value the
+    presentation switches to the next higher unit. A higher threshold factor
+    means the presentation will switch later. For example:
+
+    >>> format_timedelta(timedelta(hours=23), threshold=0.9, locale='en_US')
+    u'1 day'
+    >>> format_timedelta(timedelta(hours=23), threshold=1.1, locale='en_US')
+    u'23 hrs'
+
+    :param delta: a ``timedelta`` object representing the time difference to
+                  format, or the delta in seconds as an `int` value
+    :param granularity: determines the smallest unit that should be displayed,
+                        the value can be one of "year", "month", "week", "day",
+                        "hour", "minute" or "second"
+    :param threshold: factor that determines at which point the presentation
+                      switches to the next higher unit
+    :param locale: a `Locale` object or a locale identifier
+    :rtype: `unicode`
+    """
+    if isinstance(delta, timedelta):
+        seconds = int((delta.days * 86400) + delta.seconds)
+    else:
+        seconds = delta
+    locale = Locale.parse(locale)
+
+    for unit, secs_per_unit in TIMEDELTA_UNITS:
+        value = abs(seconds) / secs_per_unit
+        if value >= threshold or unit == granularity:
+            if unit == granularity and value > 0:
+                value = max(1, value)
+            value = int(round(value))
+            plural_form = locale.plural_form(value)
+            pattern = locale._data['unit_patterns'][unit][plural_form]
+            return pattern.replace('{0}', str(value))
+
+    return u''
+
+def parse_date(string, locale=LC_TIME):
+    """Parse a date from a string.
+    
+    This function uses the date format for the locale as a hint to determine
+    the order in which the date fields appear in the string.
+    
+    >>> parse_date('4/1/04', locale='en_US')
+    datetime.date(2004, 4, 1)
+    >>> parse_date('01.04.2004', locale='de_DE')
+    datetime.date(2004, 4, 1)
+    
+    :param string: the string containing the date
+    :param locale: a `Locale` object or a locale identifier
+    :return: the parsed date
+    :rtype: `date`
+    """
+    # TODO: try ISO format first?
+    format = get_date_format(locale=locale).pattern.lower()
+    year_idx = format.index('y')
+    month_idx = format.index('m')
+    if month_idx < 0:
+        month_idx = format.index('l')
+    day_idx = format.index('d')
+
+    indexes = [(year_idx, 'Y'), (month_idx, 'M'), (day_idx, 'D')]
+    indexes.sort()
+    indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)])
+
+    # FIXME: this currently only supports numbers, but should also support month
+    #        names, both in the requested locale, and english
+
+    numbers = re.findall('(\d+)', string)
+    year = numbers[indexes['Y']]
+    if len(year) == 2:
+        year = 2000 + int(year)
+    else:
+        year = int(year)
+    month = int(numbers[indexes['M']])
+    day = int(numbers[indexes['D']])
+    if month > 12:
+        month, day = day, month
+    return date(year, month, day)
+
+def parse_datetime(string, locale=LC_TIME):
+    """Parse a date and time from a string.
+    
+    This function uses the date and time formats for the locale as a hint to
+    determine the order in which the time fields appear in the string.
+    
+    :param string: the string containing the date and time
+    :param locale: a `Locale` object or a locale identifier
+    :return: the parsed date/time
+    :rtype: `datetime`
+    """
+    raise NotImplementedError
+
+def parse_time(string, locale=LC_TIME):
+    """Parse a time from a string.
+    
+    This function uses the time format for the locale as a hint to determine
+    the order in which the time fields appear in the string.
+    
+    >>> parse_time('15:30:00', locale='en_US')
+    datetime.time(15, 30)
+    
+    :param string: the string containing the time
+    :param locale: a `Locale` object or a locale identifier
+    :return: the parsed time
+    :rtype: `time`
+    """
+    # TODO: try ISO format first?
+    format = get_time_format(locale=locale).pattern.lower()
+    hour_idx = format.index('h')
+    if hour_idx < 0:
+        hour_idx = format.index('k')
+    min_idx = format.index('m')
+    sec_idx = format.index('s')
+
+    indexes = [(hour_idx, 'H'), (min_idx, 'M'), (sec_idx, 'S')]
+    indexes.sort()
+    indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)])
+
+    # FIXME: support 12 hour clock, and 0-based hour specification
+    #        and seconds should be optional, maybe minutes too
+    #        oh, and time-zones, of course
+
+    numbers = re.findall('(\d+)', string)
+    hour = int(numbers[indexes['H']])
+    minute = int(numbers[indexes['M']])
+    second = int(numbers[indexes['S']])
+    return time(hour, minute, second)
+
+
+class DateTimePattern(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):
+        if type(other) is not DateTimeFormat:
+            return NotImplemented
+        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))
+        if isinstance(value, (datetime, time)) and value.tzinfo is None:
+            value = value.replace(tzinfo=UTC)
+        self.value = value
+        self.locale = Locale.parse(locale)
+
+    def __getitem__(self, name):
+        char = name[0]
+        num = len(name)
+        if char == 'G':
+            return self.format_era(char, num)
+        elif char in ('y', 'Y', 'u'):
+            return self.format_year(char, num)
+        elif char in ('Q', 'q'):
+            return self.format_quarter(char, num)
+        elif char in ('M', 'L'):
+            return self.format_month(char, num)
+        elif char in ('w', 'W'):
+            return self.format_week(char, num)
+        elif char == 'd':
+            return self.format(self.value.day, num)
+        elif char == 'D':
+            return self.format_day_of_year(num)
+        elif char == 'F':
+            return self.format_day_of_week_in_month()
+        elif char in ('E', 'e', 'c'):
+            return self.format_weekday(char, num)
+        elif char == 'a':
+            return self.format_period(char)
+        elif char == 'h':
+            if self.value.hour % 12 == 0:
+                return self.format(12, num)
+            else:
+                return self.format(self.value.hour % 12, num)
+        elif char == 'H':
+            return self.format(self.value.hour, num)
+        elif char == 'K':
+            return self.format(self.value.hour % 12, num)
+        elif char == 'k':
+            if self.value.hour == 0:
+                return self.format(24, num)
+            else:
+                return self.format(self.value.hour, num)
+        elif char == 'm':
+            return self.format(self.value.minute, num)
+        elif char == 's':
+            return self.format(self.value.second, num)
+        elif char == 'S':
+            return self.format_frac_seconds(num)
+        elif char == 'A':
+            return self.format_milliseconds_in_day(num)
+        elif char in ('z', 'Z', 'v', 'V'):
+            return self.format_timezone(char, num)
+        else:
+            raise KeyError('Unsupported date/time field %r' % char)
+
+    def format_era(self, char, 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, char, num):
+        value = self.value.year
+        if char.isupper():
+            week = self.get_week_number(self.get_day_of_year())
+            if week == 0:
+                value -= 1
+        year = self.format(value, num)
+        if num == 2:
+            year = year[-2:]
+        return year
+
+    def format_quarter(self, char, num):
+        quarter = (self.value.month - 1) // 3 + 1
+        if num <= 2:
+            return ('%%0%dd' % num) % quarter
+        width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num]
+        context = {'Q': 'format', 'q': 'stand-alone'}[char]
+        return get_quarter_names(width, context, self.locale)[quarter]
+
+    def format_month(self, char, num):
+        if num <= 2:
+            return ('%%0%dd' % num) % self.value.month
+        width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num]
+        context = {'M': 'format', 'L': 'stand-alone'}[char]
+        return get_month_names(width, context, self.locale)[self.value.month]
+
+    def format_week(self, char, num):
+        if char.islower(): # week of year
+            day_of_year = self.get_day_of_year()
+            week = self.get_week_number(day_of_year)
+            if week == 0:
+                date = self.value - timedelta(days=day_of_year)
+                week = self.get_week_number(self.get_day_of_year(date),
+                                            date.weekday())
+            return self.format(week, num)
+        else: # week of month
+            week = self.get_week_number(self.value.day)
+            if week == 0:
+                date = self.value - timedelta(days=self.value.day)
+                week = self.get_week_number(date.day, date.weekday())
+                pass
+            return '%d' % week
+
+    def format_weekday(self, char, num):
+        if num < 3:
+            if char.islower():
+                value = 7 - self.locale.first_week_day + self.value.weekday()
+                return self.format(value % 7 + 1, num)
+            num = 3
+        weekday = self.value.weekday()
+        width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num]
+        context = {3: 'format', 4: 'format', 5: 'stand-alone'}[num]
+        return get_day_names(width, context, self.locale)[weekday]
+
+    def format_day_of_year(self, num):
+        return self.format(self.get_day_of_year(), num)
+
+    def format_day_of_week_in_month(self):
+        return '%d' % ((self.value.day - 1) // 7 + 1)
+
+    def format_period(self, char):
+        period = {0: 'am', 1: 'pm'}[int(self.value.hour >= 12)]
+        return get_period_names(locale=self.locale)[period]
+
+    def format_frac_seconds(self, num):
+        value = str(self.value.microsecond)
+        return self.format(round(float('.%s' % value), num) * 10**num, num)
+
+    def format_milliseconds_in_day(self, num):
+        msecs = self.value.microsecond // 1000 + self.value.second * 1000 + \
+                self.value.minute * 60000 + self.value.hour * 3600000
+        return self.format(msecs, num)
+
+    def format_timezone(self, char, num):
+        width = {3: 'short', 4: 'long'}[max(3, num)]
+        if char == 'z':
+            return get_timezone_name(self.value, width, locale=self.locale)
+        elif char == 'Z':
+            return get_timezone_gmt(self.value, width, locale=self.locale)
+        elif char == 'v':
+            return get_timezone_name(self.value.tzinfo, width,
+                                     locale=self.locale)
+        elif char == 'V':
+            if num == 1:
+                return get_timezone_name(self.value.tzinfo, width,
+                                         uncommon=True, locale=self.locale)
+            return get_timezone_location(self.value.tzinfo, locale=self.locale)
+
+    def format(self, value, length):
+        return ('%%0%dd' % length) % value
+
+    def get_day_of_year(self, date=None):
+        if date is None:
+            date = self.value
+        return (date - date_(date.year, 1, 1)).days + 1
+
+    def get_week_number(self, day_of_period, day_of_week=None):
+        """Return the number of the week of a day within a period. This may be
+        the week number in a year or the week number in a month.
+        
+        Usually this will return a value equal to or greater than 1, but if the
+        first week of the period is so short that it actually counts as the last
+        week of the previous period, this function will return 0.
+        
+        >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('de_DE'))
+        >>> format.get_week_number(6)
+        1
+        
+        >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('en_US'))
+        >>> format.get_week_number(6)
+        2
+        
+        :param day_of_period: the number of the day in the period (usually
+                              either the day of month or the day of year)
+        :param day_of_week: the week day; if ommitted, the week day of the
+                            current date is assumed
+        """
+        if day_of_week is None:
+            day_of_week = self.value.weekday()
+        first_day = (day_of_week - self.locale.first_week_day -
+                     day_of_period + 1) % 7
+        if first_day < 0:
+            first_day += 7
+        week_number = (day_of_period + first_day - 1) // 7
+        if 7 - first_day >= self.locale.min_week_days:
+            week_number += 1
+        return week_number
+
+
+PATTERN_CHARS = {
+    'G': [1, 2, 3, 4, 5],                                           # era
+    'y': None, 'Y': None, 'u': None,                                # year
+    'Q': [1, 2, 3, 4], 'q': [1, 2, 3, 4],                           # quarter
+    'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5],                     # month
+    'w': [1, 2], 'W': [1],                                          # week
+    'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None,               # day
+    'E': [1, 2, 3, 4, 5], 'e': [1, 2, 3, 4, 5], 'c': [1, 3, 4, 5],  # week day
+    'a': [1],                                                       # period
+    'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2],             # hour
+    'm': [1, 2],                                                    # minute
+    's': [1, 2], 'S': None, 'A': None,                              # second
+    'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4], 'v': [1, 4], 'V': [1, 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'
+    
+    Pattern can contain literal strings in single quotes:
+    
+    >>> parse_pattern("H:mm' Uhr 'z").format
+    u'%(H)s:%(mm)s Uhr %(z)s'
+    
+    An actual single quote can be used by using two adjacent single quote
+    characters:
+    
+    >>> parse_pattern("hh' o''clock'").format
+    u"%(hh)s o'clock"
+    
+    :param pattern: the formatting pattern to parse
+    """
+    if type(pattern) is DateTimePattern:
+        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 and fieldnum[0] not in 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.replace("''", '\0')):
+        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 == "'": # end of quote
+                charbuf.extend(quotebuf)
+                quotebuf = None
+            else: # inside quote
+                quotebuf.append(char)
+
+    if fieldchar[0]:
+        append_field()
+    elif charbuf:
+        append_chars()
+
+    return DateTimePattern(pattern, u''.join(result).replace('\0', "'"))
new file mode 100644
--- /dev/null
+++ b/babel3/babel/localedata.py
@@ -0,0 +1,209 @@
+# -*- 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/.
+
+"""Low-level locale data access.
+
+:note: The `Locale` class, which uses this module under the hood, provides a
+       more convenient interface for accessing the locale data.
+"""
+
+import os
+import pickle
+try:
+    import threading
+except ImportError:
+    import dummy_threading as threading
+from UserDict import DictMixin
+
+__all__ = ['exists', 'list', 'load']
+__docformat__ = 'restructuredtext en'
+
+_cache = {}
+_cache_lock = threading.RLock()
+_dirname = os.path.join(os.path.dirname(__file__), 'localedata')
+
+
+def exists(name):
+    """Check whether locale data is available for the given locale.
+    
+    :param name: the locale identifier string
+    :return: `True` if the locale data exists, `False` otherwise
+    :rtype: `bool`
+    """
+    if name in _cache:
+        return True
+    return os.path.exists(os.path.join(_dirname, '%s.dat' % name))
+
+
+def list():
+    """Return a list of all locale identifiers for which locale data is
+    available.
+    
+    :return: a list of locale identifiers (strings)
+    :rtype: `list`
+    :since: version 0.8.1
+    """
+    return [stem for stem, extension in [
+        os.path.splitext(filename) for filename in os.listdir(_dirname)
+    ] if extension == '.dat' and stem != 'root']
+
+
+def load(name, merge_inherited=True):
+    """Load the locale data for the given locale.
+    
+    The locale data is a dictionary that contains much of the data defined by
+    the Common Locale Data Repository (CLDR). This data is stored as a
+    collection of pickle files inside the ``babel`` package.
+    
+    >>> d = load('en_US')
+    >>> d['languages']['sv']
+    u'Swedish'
+    
+    Note that the results are cached, and subsequent requests for the same
+    locale return the same dictionary:
+    
+    >>> d1 = load('en_US')
+    >>> d2 = load('en_US')
+    >>> d1 is d2
+    True
+    
+    :param name: the locale identifier string (or "root")
+    :param merge_inherited: whether the inherited data should be merged into
+                            the data of the requested locale
+    :return: the locale data
+    :rtype: `dict`
+    :raise `IOError`: if no locale data file is found for the given locale
+                      identifer, or one of the locales it inherits from
+    """
+    _cache_lock.acquire()
+    try:
+        data = _cache.get(name)
+        if not data:
+            # Load inherited data
+            if name == 'root' or not merge_inherited:
+                data = {}
+            else:
+                parts = name.split('_')
+                if len(parts) == 1:
+                    parent = 'root'
+                else:
+                    parent = '_'.join(parts[:-1])
+                data = load(parent).copy()
+            filename = os.path.join(_dirname, '%s.dat' % name)
+            fileobj = open(filename, 'rb')
+            try:
+                if name != 'root' and merge_inherited:
+                    merge(data, pickle.load(fileobj))
+                else:
+                    data = pickle.load(fileobj)
+                _cache[name] = data
+            finally:
+                fileobj.close()
+        return data
+    finally:
+        _cache_lock.release()
+
+
+def merge(dict1, dict2):
+    """Merge the data from `dict2` into the `dict1` dictionary, making copies
+    of nested dictionaries.
+    
+    >>> d = {1: 'foo', 3: 'baz'}
+    >>> merge(d, {1: 'Foo', 2: 'Bar'})
+    >>> items = d.items(); items.sort(); items
+    [(1, 'Foo'), (2, 'Bar'), (3, 'baz')]
+    
+    :param dict1: the dictionary to merge into
+    :param dict2: the dictionary containing the data that should be merged
+    """
+    for key, val2 in dict2.items():
+        if val2 is not None:
+            val1 = dict1.get(key)
+            if isinstance(val2, dict):
+                if val1 is None:
+                    val1 = {}
+                if isinstance(val1, Alias):
+                    val1 = (val1, val2)
+                elif isinstance(val1, tuple):
+                    alias, others = val1
+                    others = others.copy()
+                    merge(others, val2)
+                    val1 = (alias, others)
+                else:
+                    val1 = val1.copy()
+                    merge(val1, val2)
+            else:
+                val1 = val2
+            dict1[key] = val1
+
+
+class Alias(object):
+    """Representation of an alias in the locale data.
+    
+    An alias is a value that refers to some other part of the locale data,
+    as specified by the `keys`.
+    """
+
+    def __init__(self, keys):
+        self.keys = tuple(keys)
+
+    def __repr__(self):
+        return '<%s %r>' % (type(self).__name__, self.keys)
+
+    def resolve(self, data):
+        """Resolve the alias based on the given data.
+        
+        This is done recursively, so if one alias resolves to a second alias,
+        that second alias will also be resolved.
+        
+        :param data: the locale data
+        :type data: `dict`
+        """
+        base = data
+        for key in self.keys:
+            data = data[key]
+        if isinstance(data, Alias):
+            data = data.resolve(base)
+        elif isinstance(data, tuple):
+            alias, others = data
+            data = alias.resolve(base)
+        return data
+
+
+class LocaleDataDict(DictMixin, dict):
+    """Dictionary wrapper that automatically resolves aliases to the actual
+    values.
+    """
+
+    def __init__(self, data, base=None):
+        dict.__init__(self, data)
+        if base is None:
+            base = data
+        self.base = base
+
+    def __getitem__(self, key):
+        orig = val = dict.__getitem__(self, key)
+        if isinstance(val, Alias): # resolve an alias
+            val = val.resolve(self.base)
+        if isinstance(val, tuple): # Merge a partial dict with an alias
+            alias, others = val
+            val = alias.resolve(self.base).copy()
+            merge(val, others)
+        if type(val) is dict: # Return a nested alias-resolving dict
+            val = LocaleDataDict(val, base=self.base)
+        if val is not orig:
+            self[key] = val
+        return val
+
+    def copy(self):
+        return LocaleDataDict(dict.copy(self), base=self.base)
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/__init__.py
@@ -0,0 +1,16 @@
+# -*- 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."""
+
+from babel.messages.catalog import *
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/catalog.py
@@ -0,0 +1,802 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2008 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/.
+
+"""Data structures for message catalogs."""
+
+from cgi import parse_header
+from datetime import datetime
+from difflib import get_close_matches
+from email import message_from_string
+from copy import copy
+import re
+import time
+
+from babel import __version__ as VERSION
+from babel.core import Locale
+from babel.dates import format_datetime
+from babel.messages.plurals import get_plural
+from babel.util import odict, distinct, set, LOCALTZ, UTC, FixedOffsetTimezone
+
+__all__ = ['Message', 'Catalog', 'TranslationError']
+__docformat__ = 'restructuredtext en'
+
+
+PYTHON_FORMAT = re.compile(r'''(?x)
+    \%
+        (?:\(([\w]*)\))?
+        (
+            [-#0\ +]?(?:\*|[\d]+)?
+            (?:\.(?:\*|[\d]+))?
+            [hlL]?
+        )
+        ([diouxXeEfFgGcrs%])
+''')
+
+
+class Message(object):
+    """Representation of a single message in a catalog."""
+
+    def __init__(self, id, string=u'', locations=(), flags=(), auto_comments=(),
+                 user_comments=(), previous_id=(), lineno=None, context=None):
+        """Create the message object.
+
+        :param id: the message ID, or a ``(singular, plural)`` tuple for
+                   pluralizable messages
+        :param string: the translated message string, or a
+                       ``(singular, plural)`` tuple for pluralizable messages
+        :param locations: a sequence of ``(filenname, lineno)`` tuples
+        :param flags: a set or sequence of flags
+        :param auto_comments: a sequence of automatic comments for the message
+        :param user_comments: a sequence of user comments for the message
+        :param previous_id: the previous message ID, or a ``(singular, plural)``
+                            tuple for pluralizable messages
+        :param lineno: the line number on which the msgid line was found in the
+                       PO file, if any
+        :param context: the message context
+        """
+        self.id = id #: The message ID
+        if not string and self.pluralizable:
+            string = (u'', u'')
+        self.string = string #: The message translation
+        self.locations = list(distinct(locations))
+        self.flags = set(flags)
+        if id and self.python_format:
+            self.flags.add('python-format')
+        else:
+            self.flags.discard('python-format')
+        self.auto_comments = list(distinct(auto_comments))
+        self.user_comments = list(distinct(user_comments))
+        if isinstance(previous_id, basestring):
+            self.previous_id = [previous_id]
+        else:
+            self.previous_id = list(previous_id)
+        self.lineno = lineno
+        self.context = context
+
+    def __repr__(self):
+        return '<%s %r (flags: %r)>' % (type(self).__name__, self.id,
+                                        list(self.flags))
+
+    def __cmp__(self, obj):
+        """Compare Messages, taking into account plural ids"""
+        if isinstance(obj, Message):
+            plural = self.pluralizable
+            obj_plural = obj.pluralizable
+            if plural and obj_plural:
+                return cmp(self.id[0], obj.id[0])
+            elif plural:
+                return cmp(self.id[0], obj.id)
+            elif obj_plural:
+                return cmp(self.id, obj.id[0])
+        return cmp(self.id, obj.id)
+
+    def clone(self):
+        return Message(*map(copy, (self.id, self.string, self.locations,
+                                   self.flags, self.auto_comments,
+                                   self.user_comments, self.previous_id,
+                                   self.lineno, self.context)))
+
+    def check(self, catalog=None):
+        """Run various validation checks on the message.  Some validations
+        are only performed if the catalog is provided.  This method returns
+        a sequence of `TranslationError` objects.
+
+        :rtype: ``iterator``
+        :param catalog: A catalog instance that is passed to the checkers
+        :see: `Catalog.check` for a way to perform checks for all messages
+              in a catalog.
+        """
+        from babel.messages.checkers import checkers
+        errors = []
+        for checker in checkers:
+            try:
+                checker(catalog, self)
+            except TranslationError, e:
+                errors.append(e)
+        return errors
+
+    def fuzzy(self):
+        return 'fuzzy' in self.flags
+    fuzzy = property(fuzzy, doc="""\
+        Whether the translation is fuzzy.
+
+        >>> Message('foo').fuzzy
+        False
+        >>> msg = Message('foo', 'foo', flags=['fuzzy'])
+        >>> msg.fuzzy
+        True
+        >>> msg
+        <Message 'foo' (flags: ['fuzzy'])>
+
+        :type:  `bool`
+        """)
+
+    def pluralizable(self):
+        return isinstance(self.id, (list, tuple))
+    pluralizable = property(pluralizable, doc="""\
+        Whether the message is plurizable.
+
+        >>> Message('foo').pluralizable
+        False
+        >>> Message(('foo', 'bar')).pluralizable
+        True
+
+        :type:  `bool`
+        """)
+
+    def python_format(self):
+        ids = self.id
+        if not isinstance(ids, (list, tuple)):
+            ids = [ids]
+        return bool(filter(None, [PYTHON_FORMAT.search(id) for id in ids]))
+    python_format = property(python_format, doc="""\
+        Whether the message contains Python-style parameters.
+
+        >>> Message('foo %(name)s bar').python_format
+        True
+        >>> Message(('foo %(name)s', 'foo %(name)s')).python_format
+        True
+
+        :type:  `bool`
+        """)
+
+
+class TranslationError(Exception):
+    """Exception thrown by translation checkers when invalid message
+    translations are encountered."""
+
+
+DEFAULT_HEADER = u"""\
+# Translations template for PROJECT.
+# Copyright (C) YEAR ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#"""
+
+
+class Catalog(object):
+    """Representation of a message catalog."""
+
+    def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER,
+                 project=None, version=None, copyright_holder=None,
+                 msgid_bugs_address=None, creation_date=None,
+                 revision_date=None, last_translator=None, language_team=None,
+                 charset='utf-8', fuzzy=True):
+        """Initialize the catalog object.
+
+        :param locale: the locale identifier or `Locale` object, or `None`
+                       if the catalog is not bound to a locale (which basically
+                       means it's a template)
+        :param domain: the message domain
+        :param header_comment: the header comment as string, or `None` for the
+                               default header
+        :param project: the project's name
+        :param version: the project's version
+        :param copyright_holder: the copyright holder of the catalog
+        :param msgid_bugs_address: the email address or URL to submit bug
+                                   reports to
+        :param creation_date: the date the catalog was created
+        :param revision_date: the date the catalog was revised
+        :param last_translator: the name and email of the last translator
+        :param language_team: the name and email of the language team
+        :param charset: the encoding to use in the output
+        :param fuzzy: the fuzzy bit on the catalog header
+        """
+        self.domain = domain #: The message domain
+        if locale:
+            locale = Locale.parse(locale)
+        self.locale = locale #: The locale or `None`
+        self._header_comment = header_comment
+        self._messages = odict()
+
+        self.project = project or 'PROJECT' #: The project name
+        self.version = version or 'VERSION' #: The project version
+        self.copyright_holder = copyright_holder or 'ORGANIZATION'
+        self.msgid_bugs_address = msgid_bugs_address or 'EMAIL@ADDRESS'
+
+        self.last_translator = last_translator or 'FULL NAME <EMAIL@ADDRESS>'
+        """Name and email address of the last translator."""
+        self.language_team = language_team or 'LANGUAGE <LL@li.org>'
+        """Name and email address of the language team."""
+
+        self.charset = charset or 'utf-8'
+
+        if creation_date is None:
+            creation_date = datetime.now(LOCALTZ)
+        elif isinstance(creation_date, datetime) and not creation_date.tzinfo:
+            creation_date = creation_date.replace(tzinfo=LOCALTZ)
+        self.creation_date = creation_date #: Creation date of the template
+        if revision_date is None:
+            revision_date = datetime.now(LOCALTZ)
+        elif isinstance(revision_date, datetime) and not revision_date.tzinfo:
+            revision_date = revision_date.replace(tzinfo=LOCALTZ)
+        self.revision_date = revision_date #: Last revision date of the catalog
+        self.fuzzy = fuzzy #: Catalog header fuzzy bit (`True` or `False`)
+
+        self.obsolete = odict() #: Dictionary of obsolete messages
+        self._num_plurals = None
+        self._plural_expr = None
+
+    def _get_header_comment(self):
+        comment = self._header_comment
+        comment = comment.replace('PROJECT', self.project) \
+                         .replace('VERSION', self.version) \
+                         .replace('YEAR', self.revision_date.strftime('%Y')) \
+                         .replace('ORGANIZATION', self.copyright_holder)
+        if self.locale:
+            comment = comment.replace('Translations template', '%s translations'
+                                      % self.locale.english_name)
+        return comment
+
+    def _set_header_comment(self, string):
+        self._header_comment = string
+
+    header_comment = property(_get_header_comment, _set_header_comment, doc="""\
+    The header comment for the catalog.
+
+    >>> catalog = Catalog(project='Foobar', version='1.0',
+    ...                   copyright_holder='Foo Company')
+    >>> print catalog.header_comment #doctest: +ELLIPSIS
+    # Translations template for Foobar.
+    # Copyright (C) ... Foo Company
+    # This file is distributed under the same license as the Foobar project.
+    # FIRST AUTHOR <EMAIL@ADDRESS>, ....
+    #
+
+    The header can also be set from a string. Any known upper-case variables
+    will be replaced when the header is retrieved again:
+
+    >>> catalog = Catalog(project='Foobar', version='1.0',
+    ...                   copyright_holder='Foo Company')
+    >>> catalog.header_comment = '''\\
+    ... # The POT for my really cool PROJECT project.
+    ... # Copyright (C) 1990-2003 ORGANIZATION
+    ... # This file is distributed under the same license as the PROJECT
+    ... # project.
+    ... #'''
+    >>> print catalog.header_comment
+    # The POT for my really cool Foobar project.
+    # Copyright (C) 1990-2003 Foo Company
+    # This file is distributed under the same license as the Foobar
+    # project.
+    #
+
+    :type: `unicode`
+    """)
+
+    def _get_mime_headers(self):
+        headers = []
+        headers.append(('Project-Id-Version',
+                        '%s %s' % (self.project, self.version)))
+        headers.append(('Report-Msgid-Bugs-To', self.msgid_bugs_address))
+        headers.append(('POT-Creation-Date',
+                        format_datetime(self.creation_date, 'yyyy-MM-dd HH:mmZ',
+                                        locale='en')))
+        if self.locale is None:
+            headers.append(('PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE'))
+            headers.append(('Last-Translator', 'FULL NAME <EMAIL@ADDRESS>'))
+            headers.append(('Language-Team', 'LANGUAGE <LL@li.org>'))
+        else:
+            headers.append(('PO-Revision-Date',
+                            format_datetime(self.revision_date,
+                                            'yyyy-MM-dd HH:mmZ', locale='en')))
+            headers.append(('Last-Translator', self.last_translator))
+            headers.append(('Language-Team',
+                           self.language_team.replace('LANGUAGE',
+                                                      str(self.locale))))
+            headers.append(('Plural-Forms', self.plural_forms))
+        headers.append(('MIME-Version', '1.0'))
+        headers.append(('Content-Type',
+                        'text/plain; charset=%s' % self.charset))
+        headers.append(('Content-Transfer-Encoding', '8bit'))
+        headers.append(('Generated-By', 'Babel %s\n' % VERSION))
+        return headers
+
+    def _set_mime_headers(self, headers):
+        for name, value in headers:
+            if name.lower() == 'content-type':
+                mimetype, params = parse_header(value)
+                if 'charset' in params:
+                    self.charset = params['charset'].lower()
+                break
+        for name, value in headers:
+            name = name.lower().decode(self.charset)
+            value = value.decode(self.charset)
+            if name == 'project-id-version':
+                parts = value.split(' ')
+                self.project = u' '.join(parts[:-1])
+                self.version = parts[-1]
+            elif name == 'report-msgid-bugs-to':
+                self.msgid_bugs_address = value
+            elif name == 'last-translator':
+                self.last_translator = value
+            elif name == 'language-team':
+                self.language_team = value
+            elif name == 'plural-forms':
+                _, params = parse_header(' ;' + value)
+                self._num_plurals = int(params.get('nplurals', 2))
+                self._plural_expr = params.get('plural', '(n != 1)')
+            elif name == 'pot-creation-date':
+                # FIXME: this should use dates.parse_datetime as soon as that
+                #        is ready
+                value, tzoffset, _ = re.split('([+-]\d{4})$', value, 1)
+
+                tt = time.strptime(value, '%Y-%m-%d %H:%M')
+                ts = time.mktime(tt)
+
+                # Separate the offset into a sign component, hours, and minutes
+                plus_minus_s, rest = tzoffset[0], tzoffset[1:]
+                hours_offset_s, mins_offset_s = rest[:2], rest[2:]
+
+                # Make them all integers
+                plus_minus = int(plus_minus_s + '1')
+                hours_offset = int(hours_offset_s)
+                mins_offset = int(mins_offset_s)
+
+                # Calculate net offset
+                net_mins_offset = hours_offset * 60
+                net_mins_offset += mins_offset
+                net_mins_offset *= plus_minus
+
+                # Create an offset object
+                tzoffset = FixedOffsetTimezone(net_mins_offset)
+
+                # Store the offset in a datetime object
+                dt = datetime.fromtimestamp(ts)
+                self.creation_date = dt.replace(tzinfo=tzoffset)
+            elif name == 'po-revision-date':
+                # Keep the value if it's not the default one
+                if 'YEAR' not in value:
+                    # FIXME: this should use dates.parse_datetime as soon as
+                    #        that is ready
+                    value, tzoffset, _ = re.split('([+-]\d{4})$', value, 1)
+                    tt = time.strptime(value, '%Y-%m-%d %H:%M')
+                    ts = time.mktime(tt)
+
+                    # Separate the offset into a sign component, hours, and
+                    # minutes
+                    plus_minus_s, rest = tzoffset[0], tzoffset[1:]
+                    hours_offset_s, mins_offset_s = rest[:2], rest[2:]
+
+                    # Make them all integers
+                    plus_minus = int(plus_minus_s + '1')
+                    hours_offset = int(hours_offset_s)
+                    mins_offset = int(mins_offset_s)
+
+                    # Calculate net offset
+                    net_mins_offset = hours_offset * 60
+                    net_mins_offset += mins_offset
+                    net_mins_offset *= plus_minus
+
+                    # Create an offset object
+                    tzoffset = FixedOffsetTimezone(net_mins_offset)
+
+                    # Store the offset in a datetime object
+                    dt = datetime.fromtimestamp(ts)
+                    self.revision_date = dt.replace(tzinfo=tzoffset)
+
+    mime_headers = property(_get_mime_headers, _set_mime_headers, doc="""\
+    The MIME headers of the catalog, used for the special ``msgid ""`` entry.
+
+    The behavior of this property changes slightly depending on whether a locale
+    is set or not, the latter indicating that the catalog is actually a template
+    for actual translations.
+
+    Here's an example of the output for such a catalog template:
+
+    >>> created = datetime(1990, 4, 1, 15, 30, tzinfo=UTC)
+    >>> catalog = Catalog(project='Foobar', version='1.0',
+    ...                   creation_date=created)
+    >>> for name, value in catalog.mime_headers:
+    ...     print '%s: %s' % (name, value)
+    Project-Id-Version: Foobar 1.0
+    Report-Msgid-Bugs-To: EMAIL@ADDRESS
+    POT-Creation-Date: 1990-04-01 15:30+0000
+    PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
+    Last-Translator: FULL NAME <EMAIL@ADDRESS>
+    Language-Team: LANGUAGE <LL@li.org>
+    MIME-Version: 1.0
+    Content-Type: text/plain; charset=utf-8
+    Content-Transfer-Encoding: 8bit
+    Generated-By: Babel ...
+
+    And here's an example of the output when the locale is set:
+
+    >>> revised = datetime(1990, 8, 3, 12, 0, tzinfo=UTC)
+    >>> catalog = Catalog(locale='de_DE', project='Foobar', version='1.0',
+    ...                   creation_date=created, revision_date=revised,
+    ...                   last_translator='John Doe <jd@example.com>',
+    ...                   language_team='de_DE <de@example.com>')
+    >>> for name, value in catalog.mime_headers:
+    ...     print '%s: %s' % (name, value)
+    Project-Id-Version: Foobar 1.0
+    Report-Msgid-Bugs-To: EMAIL@ADDRESS
+    POT-Creation-Date: 1990-04-01 15:30+0000
+    PO-Revision-Date: 1990-08-03 12:00+0000
+    Last-Translator: John Doe <jd@example.com>
+    Language-Team: de_DE <de@example.com>
+    Plural-Forms: nplurals=2; plural=(n != 1)
+    MIME-Version: 1.0
+    Content-Type: text/plain; charset=utf-8
+    Content-Transfer-Encoding: 8bit
+    Generated-By: Babel ...
+
+    :type: `list`
+    """)
+
+    def num_plurals(self):
+        if self._num_plurals is None:
+            num = 2
+            if self.locale:
+                num = get_plural(self.locale)[0]
+            self._num_plurals = num
+        return self._num_plurals
+    num_plurals = property(num_plurals, doc="""\
+    The number of plurals used by the catalog or locale.
+
+    >>> Catalog(locale='en').num_plurals
+    2
+    >>> Catalog(locale='ga').num_plurals
+    3
+
+    :type: `int`
+    """)
+
+    def plural_expr(self):
+        if self._plural_expr is None:
+            expr = '(n != 1)'
+            if self.locale:
+                expr = get_plural(self.locale)[1]
+            self._plural_expr = expr
+        return self._plural_expr
+    plural_expr = property(plural_expr, doc="""\
+    The plural expression used by the catalog or locale.
+
+    >>> Catalog(locale='en').plural_expr
+    '(n != 1)'
+    >>> Catalog(locale='ga').plural_expr
+    '(n==1 ? 0 : n==2 ? 1 : 2)'
+
+    :type: `basestring`
+    """)
+
+    def plural_forms(self):
+        return 'nplurals=%s; plural=%s' % (self.num_plurals, self.plural_expr)
+    plural_forms = property(plural_forms, doc="""\
+    Return the plural forms declaration for the locale.
+
+    >>> Catalog(locale='en').plural_forms
+    'nplurals=2; plural=(n != 1)'
+    >>> Catalog(locale='pt_BR').plural_forms
+    'nplurals=2; plural=(n > 1)'
+
+    :type: `str`
+    """)
+
+    def __contains__(self, id):
+        """Return whether the catalog has a message with the specified ID."""
+        return self._key_for(id) in self._messages
+
+    def __len__(self):
+        """The number of messages in the catalog.
+
+        This does not include the special ``msgid ""`` entry.
+        """
+        return len(self._messages)
+
+    def __iter__(self):
+        """Iterates through all the entries in the catalog, in the order they
+        were added, yielding a `Message` object for every entry.
+
+        :rtype: ``iterator``
+        """
+        buf = []
+        for name, value in self.mime_headers:
+            buf.append('%s: %s' % (name, value))
+        flags = set()
+        if self.fuzzy:
+            flags |= set(['fuzzy'])
+        yield Message(u'', '\n'.join(buf), flags=flags)
+        for key in self._messages:
+            yield self._messages[key]
+
+    def __repr__(self):
+        locale = ''
+        if self.locale:
+            locale = ' %s' % self.locale
+        return '<%s %r%s>' % (type(self).__name__, self.domain, locale)
+
+    def __delitem__(self, id):
+        """Delete the message with the specified ID."""
+        self.delete(id)
+
+    def __getitem__(self, id):
+        """Return the message with the specified ID.
+
+        :param id: the message ID
+        :return: the message with the specified ID, or `None` if no such
+                 message is in the catalog
+        :rtype: `Message`
+        """
+        return self.get(id)
+
+    def __setitem__(self, id, message):
+        """Add or update the message with the specified ID.
+
+        >>> catalog = Catalog()
+        >>> catalog[u'foo'] = Message(u'foo')
+        >>> catalog[u'foo']
+        <Message u'foo' (flags: [])>
+
+        If a message with that ID is already in the catalog, it is updated
+        to include the locations and flags of the new message.
+
+        >>> catalog = Catalog()
+        >>> catalog[u'foo'] = Message(u'foo', locations=[('main.py', 1)])
+        >>> catalog[u'foo'].locations
+        [('main.py', 1)]
+        >>> catalog[u'foo'] = Message(u'foo', locations=[('utils.py', 5)])
+        >>> catalog[u'foo'].locations
+        [('main.py', 1), ('utils.py', 5)]
+
+        :param id: the message ID
+        :param message: the `Message` object
+        """
+        assert isinstance(message, Message), 'expected a Message object'
+        key = self._key_for(id, message.context)
+        current = self._messages.get(key)
+        if current:
+            if message.pluralizable and not current.pluralizable:
+                # The new message adds pluralization
+                current.id = message.id
+                current.string = message.string
+            current.locations = list(distinct(current.locations +
+                                              message.locations))
+            current.auto_comments = list(distinct(current.auto_comments +
+                                                  message.auto_comments))
+            current.user_comments = list(distinct(current.user_comments +
+                                                  message.user_comments))
+            current.flags |= message.flags
+            message = current
+        elif id == '':
+            # special treatment for the header message
+            headers = message_from_string(message.string.encode(self.charset))
+            self.mime_headers = headers.items()
+            self.header_comment = '\n'.join(['# %s' % comment for comment
+                                             in message.user_comments])
+            self.fuzzy = message.fuzzy
+        else:
+            if isinstance(id, (list, tuple)):
+                assert isinstance(message.string, (list, tuple)), \
+                    'Expected sequence but got %s' % type(message.string)
+            self._messages[key] = message
+
+    def add(self, id, string=None, locations=(), flags=(), auto_comments=(),
+            user_comments=(), previous_id=(), lineno=None, context=None):
+        """Add or update the message with the specified ID.
+
+        >>> catalog = Catalog()
+        >>> catalog.add(u'foo')
+        >>> catalog[u'foo']
+        <Message u'foo' (flags: [])>
+
+        This method simply constructs a `Message` object with the given
+        arguments and invokes `__setitem__` with that object.
+
+        :param id: the message ID, or a ``(singular, plural)`` tuple for
+                   pluralizable messages
+        :param string: the translated message string, or a
+                       ``(singular, plural)`` tuple for pluralizable messages
+        :param locations: a sequence of ``(filenname, lineno)`` tuples
+        :param flags: a set or sequence of flags
+        :param auto_comments: a sequence of automatic comments
+        :param user_comments: a sequence of user comments
+        :param previous_id: the previous message ID, or a ``(singular, plural)``
+                            tuple for pluralizable messages
+        :param lineno: the line number on which the msgid line was found in the
+                       PO file, if any
+        :param context: the message context
+        """
+        self[id] = Message(id, string, list(locations), flags, auto_comments,
+                           user_comments, previous_id, lineno=lineno,
+                           context=context)
+
+    def check(self):
+        """Run various validation checks on the translations in the catalog.
+
+        For every message which fails validation, this method yield a
+        ``(message, errors)`` tuple, where ``message`` is the `Message` object
+        and ``errors`` is a sequence of `TranslationError` objects.
+
+        :rtype: ``iterator``
+        """
+        for message in self._messages.values():
+            errors = message.check(catalog=self)
+            if errors:
+                yield message, errors
+
+    def get(self, id, context=None):
+        """Return the message with the specified ID and context.
+
+        :param id: the message ID
+        :param context: the message context, or ``None`` for no context
+        :return: the message with the specified ID, or `None` if no such
+                 message is in the catalog
+        :rtype: `Message`
+        """
+        return self._messages.get(self._key_for(id, context))
+
+    def delete(self, id, context=None):
+        """Delete the message with the specified ID and context.
+        
+        :param id: the message ID
+        :param context: the message context, or ``None`` for no context
+        """
+        key = self._key_for(id, context)
+        if key in self._messages:
+            del self._messages[key]
+
+    def update(self, template, no_fuzzy_matching=False):
+        """Update the catalog based on the given template catalog.
+
+        >>> from babel.messages import Catalog
+        >>> template = Catalog()
+        >>> template.add('green', locations=[('main.py', 99)])
+        >>> template.add('blue', locations=[('main.py', 100)])
+        >>> template.add(('salad', 'salads'), locations=[('util.py', 42)])
+        >>> catalog = Catalog(locale='de_DE')
+        >>> catalog.add('blue', u'blau', locations=[('main.py', 98)])
+        >>> catalog.add('head', u'Kopf', locations=[('util.py', 33)])
+        >>> catalog.add(('salad', 'salads'), (u'Salat', u'Salate'),
+        ...             locations=[('util.py', 38)])
+
+        >>> catalog.update(template)
+        >>> len(catalog)
+        3
+
+        >>> msg1 = catalog['green']
+        >>> msg1.string
+        >>> msg1.locations
+        [('main.py', 99)]
+
+        >>> msg2 = catalog['blue']
+        >>> msg2.string
+        u'blau'
+        >>> msg2.locations
+        [('main.py', 100)]
+
+        >>> msg3 = catalog['salad']
+        >>> msg3.string
+        (u'Salat', u'Salate')
+        >>> msg3.locations
+        [('util.py', 42)]
+
+        Messages that are in the catalog but not in the template are removed
+        from the main collection, but can still be accessed via the `obsolete`
+        member:
+
+        >>> 'head' in catalog
+        False
+        >>> catalog.obsolete.values()
+        [<Message 'head' (flags: [])>]
+
+        :param template: the reference catalog, usually read from a POT file
+        :param no_fuzzy_matching: whether to use fuzzy matching of message IDs
+        """
+        messages = self._messages
+        remaining = messages.copy()
+        self._messages = odict()
+
+        # Prepare for fuzzy matching
+        fuzzy_candidates = []
+        if not no_fuzzy_matching:
+            fuzzy_candidates = dict([
+                (self._key_for(msgid), messages[msgid].context)
+                for msgid in messages if msgid and messages[msgid].string
+            ])
+        fuzzy_matches = set()
+
+        def _merge(message, oldkey, newkey):
+            message = message.clone()
+            fuzzy = False
+            if oldkey != newkey:
+                fuzzy = True
+                fuzzy_matches.add(oldkey)
+                oldmsg = messages.get(oldkey)
+                if isinstance(oldmsg.id, basestring):
+                    message.previous_id = [oldmsg.id]
+                else:
+                    message.previous_id = list(oldmsg.id)
+            else:
+                oldmsg = remaining.pop(oldkey, None)
+            message.string = oldmsg.string
+            if isinstance(message.id, (list, tuple)):
+                if not isinstance(message.string, (list, tuple)):
+                    fuzzy = True
+                    message.string = tuple(
+                        [message.string] + ([u''] * (len(message.id) - 1))
+                    )
+                elif len(message.string) != self.num_plurals:
+                    fuzzy = True
+                    message.string = tuple(message.string[:len(oldmsg.string)])
+            elif isinstance(message.string, (list, tuple)):
+                fuzzy = True
+                message.string = message.string[0]
+            message.flags |= oldmsg.flags
+            if fuzzy:
+                message.flags |= set([u'fuzzy'])
+            self[message.id] = message
+
+        for message in template:
+            if message.id:
+                key = self._key_for(message.id, message.context)
+                if key in messages:
+                    _merge(message, key, key)
+                else:
+                    if no_fuzzy_matching is False:
+                        # do some fuzzy matching with difflib
+                        if isinstance(key, tuple):
+                            matchkey = key[0] # just the msgid, no context
+                        else:
+                            matchkey = key
+                        matches = get_close_matches(matchkey.lower().strip(),
+                                                    fuzzy_candidates.keys(), 1)
+                        if matches:
+                            newkey = matches[0]
+                            newctxt = fuzzy_candidates[newkey]
+                            if newctxt is not None:
+                                newkey = newkey, newctxt
+                            _merge(message, newkey, key)
+                            continue
+
+                    self[message.id] = message
+
+        self.obsolete = odict()
+        for msgid in remaining:
+            if no_fuzzy_matching or msgid not in fuzzy_matches:
+                self.obsolete[msgid] = remaining[msgid]
+        # Make updated catalog's POT-Creation-Date equal to the template
+        # used to update the catalog
+        self.creation_date = template.creation_date
+
+    def _key_for(self, id, context=None):
+        """The key for a message is just the singular ID even for pluralizable
+        messages, but is a ``(msgid, msgctxt)`` tuple for context-specific
+        messages.
+        """
+        key = id
+        if isinstance(key, (list, tuple)):
+            key = id[0]
+        if context is not None:
+            key = (key, context)
+        return key
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/checkers.py
@@ -0,0 +1,174 @@
+# -*- 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 routines that help with validation of translations.
+
+:since: version 0.9
+"""
+
+from itertools import izip
+from babel.messages.catalog import TranslationError, PYTHON_FORMAT
+from babel.util import set
+
+#: list of format chars that are compatible to each other
+_string_format_compatibilities = [
+    set(['i', 'd', 'u']),
+    set(['x', 'X']),
+    set(['f', 'F', 'g', 'G'])
+]
+
+
+def num_plurals(catalog, message):
+    """Verify the number of plurals in the translation."""
+    if not message.pluralizable:
+        if not isinstance(message.string, basestring):
+            raise TranslationError("Found plural forms for non-pluralizable "
+                                   "message")
+        return
+
+    # skip further tests if no catalog is provided.
+    elif catalog is None:
+        return
+
+    msgstrs = message.string
+    if not isinstance(msgstrs, (list, tuple)):
+        msgstrs = (msgstrs,)
+    if len(msgstrs) != catalog.num_plurals:
+        raise TranslationError("Wrong number of plural forms (expected %d)" %
+                               catalog.num_plurals)
+
+
+def python_format(catalog, message):
+    """Verify the format string placeholders in the translation."""
+    if 'python-format' not in message.flags:
+        return
+    msgids = message.id
+    if not isinstance(msgids, (list, tuple)):
+        msgids = (msgids,)
+    msgstrs = message.string
+    if not isinstance(msgstrs, (list, tuple)):
+        msgstrs = (msgstrs,)
+
+    for msgid, msgstr in izip(msgids, msgstrs):
+        if msgstr:
+            _validate_format(msgid, msgstr)
+
+
+def _validate_format(format, alternative):
+    """Test format string `alternative` against `format`.  `format` can be the
+    msgid of a message and `alternative` one of the `msgstr`\s.  The two
+    arguments are not interchangeable as `alternative` may contain less
+    placeholders if `format` uses named placeholders.
+
+    The behavior of this function is undefined if the string does not use
+    string formattings.
+
+    If the string formatting of `alternative` is compatible to `format` the
+    function returns `None`, otherwise a `TranslationError` is raised.
+
+    Examples for compatible format strings:
+
+    >>> _validate_format('Hello %s!', 'Hallo %s!')
+    >>> _validate_format('Hello %i!', 'Hallo %d!')
+
+    Example for an incompatible format strings:
+
+    >>> _validate_format('Hello %(name)s!', 'Hallo %s!')
+    Traceback (most recent call last):
+      ...
+    TranslationError: the format strings are of different kinds
+
+    This function is used by the `python_format` checker.
+
+    :param format: The original format string
+    :param alternative: The alternative format string that should be checked
+                        against format
+    :return: None on success
+    :raises TranslationError: on formatting errors
+    """
+
+    def _parse(string):
+        result = []
+        for match in PYTHON_FORMAT.finditer(string):
+            name, format, typechar = match.groups()
+            if typechar == '%' and name is None:
+                continue
+            result.append((name, str(typechar)))
+        return result
+
+    def _compatible(a, b):
+        if a == b:
+            return True
+        for set in _string_format_compatibilities:
+            if a in set and b in set:
+                return True
+        return False
+
+    def _check_positional(results):
+        positional = None
+        for name, char in results:
+            if positional is None:
+                positional = name is None
+            else:
+                if (name is None) != positional:
+                    raise TranslationError('format string mixes positional '
+                                           'and named placeholders')
+        return bool(positional)
+
+    a, b = map(_parse, (format, alternative))
+
+    # now check if both strings are positional or named
+    a_positional, b_positional = map(_check_positional, (a, b))
+    if a_positional and not b_positional and not b:
+        raise TranslationError('placeholders are incompatible')
+    elif a_positional != b_positional:
+        raise TranslationError('the format strings are of different kinds')
+
+    # if we are operating on positional strings both must have the
+    # same number of format chars and those must be compatible
+    if a_positional:
+        if len(a) != len(b):
+            raise TranslationError('positional format placeholders are '
+                                   'unbalanced')
+        for idx, ((_, first), (_, second)) in enumerate(izip(a, b)):
+            if not _compatible(first, second):
+                raise TranslationError('incompatible format for placeholder '
+                                       '%d: %r and %r are not compatible' %
+                                       (idx + 1, first, second))
+
+    # otherwise the second string must not have names the first one
+    # doesn't have and the types of those included must be compatible
+    else:
+        type_map = dict(a)
+        for name, typechar in b:
+            if name not in type_map:
+                raise TranslationError('unknown named placeholder %r' % name)
+            elif not _compatible(typechar, type_map[name]):
+                raise TranslationError('incompatible format for '
+                                       'placeholder %r: '
+                                       '%r and %r are not compatible' %
+                                       (name, typechar, type_map[name]))
+
+
+def _find_checkers():
+    try:
+        from pkg_resources import working_set
+    except ImportError:
+        return [num_plurals, python_format]
+    checkers = []
+    for entry_point in working_set.iter_entry_points('babel.checkers'):
+        checkers.append(entry_point.load())
+    return checkers
+
+
+checkers = _find_checkers()
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/extract.py
@@ -0,0 +1,550 @@
+# -*- 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
+import sys
+from tokenize import generate_tokens, COMMENT, NAME, OP, STRING
+
+from babel.util import parse_encoding, pathmatch, relpath, set
+from textwrap import dedent
+
+__all__ = ['extract', 'extract_from_dir', 'extract_from_file']
+__docformat__ = 'restructuredtext en'
+
+GROUP_NAME = 'babel.extractors'
+
+DEFAULT_KEYWORDS = {
+    '_': None,
+    'gettext': None,
+    'ngettext': (1, 2),
+    'ugettext': None,
+    'ungettext': (1, 2),
+    'dgettext': (2,),
+    'dngettext': (2, 3),
+    'N_': None
+}
+
+DEFAULT_MAPPING = [('**.py', 'python')]
+
+empty_msgid_warning = (
+'%s: warning: Empty msgid.  It is reserved by GNU gettext: gettext("") '
+'returns the header entry with meta information, not the empty string.')
+
+
+def _strip_comment_tags(comments, tags):
+    """Helper function for `extract` that strips comment tags from strings
+    in a list of comment lines.  This functions operates in-place.
+    """
+    def _strip(line):
+        for tag in tags:
+            if line.startswith(tag):
+                return line[len(tag):].strip()
+        return line
+    comments[:] = map(_strip, comments)
+
+
+def extract_from_dir(dirname=os.getcwd(), method_map=DEFAULT_MAPPING,
+                     options_map=None, keywords=DEFAULT_KEYWORDS,
+                     comment_tags=(), callback=None, strip_comment_tags=False):
+    """Extract messages from any source files found in the given directory.
+
+    This function generates tuples of the form:
+
+        ``(filename, lineno, message, comments)``
+
+    Which extraction method is used per file is determined by the `method_map`
+    parameter, which maps extended glob patterns to extraction method names.
+    For example, the following is the default mapping:
+
+    >>> method_map = [
+    ...     ('**.py', 'python')
+    ... ]
+
+    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 mapping patterns are ignored. See
+    the documentation of the `pathmatch` function for details on the pattern
+    syntax.
+
+    The following extended mapping would also use the "genshi" extraction
+    method on any file in "templates" subdirectory:
+
+    >>> method_map = [
+    ...     ('**/templates/**.*', 'genshi'),
+    ...     ('**.py', 'python')
+    ... ]
+
+    The dictionary provided by the optional `options_map` parameter augments
+    these mappings. It uses extended glob patterns as keys, and the values are
+    dictionaries mapping options names to option values (both strings).
+
+    The glob patterns of the `options_map` do not necessarily need to be the
+    same as those used in the method mapping. For example, while all files in
+    the ``templates`` folders in an application may be Genshi applications, the
+    options for those files may differ based on extension:
+
+    >>> options_map = {
+    ...     '**/templates/**.txt': {
+    ...         'template_class': 'genshi.template:TextTemplate',
+    ...         'encoding': 'latin-1'
+    ...     },
+    ...     '**/templates/**.html': {
+    ...         'include_attrs': ''
+    ...     }
+    ... }
+
+    :param dirname: the path to the directory to extract messages from
+    :param method_map: a list of ``(pattern, method)`` tuples that maps of
+                       extraction method names to extended glob patterns
+    :param options_map: a dictionary of additional options (optional)
+    :param keywords: a dictionary mapping keywords (i.e. names of functions
+                     that should be recognized as translation functions) to
+                     tuples that specify which of their arguments contain
+                     localizable strings
+    :param comment_tags: a list of tags of translator comments to search for
+                         and include in the results
+    :param callback: a function that is called for every file that message are
+                     extracted from, just before the extraction itself is
+                     performed; the function is passed the filename, the name
+                     of the extraction method and and the options dictionary as
+                     positional arguments, in that order
+    :param strip_comment_tags: a flag that if set to `True` causes all comment
+                               tags to be removed from the collected comments.
+    :return: an iterator over ``(filename, lineno, funcname, message)`` tuples
+    :rtype: ``iterator``
+    :see: `pathmatch`
+    """
+    if options_map is None:
+        options_map = {}
+
+    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)
+        dirnames.sort()
+        filenames.sort()
+        for filename in filenames:
+            filename = relpath(
+                os.path.join(root, filename).replace(os.sep, '/'),
+                dirname
+            )
+            for pattern, method in method_map:
+                if pathmatch(pattern, filename):
+                    filepath = os.path.join(absname, filename)
+                    options = {}
+                    for opattern, odict in options_map.items():
+                        if pathmatch(opattern, filename):
+                            options = odict
+                    if callback:
+                        callback(filename, method, options)
+                    for lineno, message, comments in \
+                          extract_from_file(method, filepath,
+                                            keywords=keywords,
+                                            comment_tags=comment_tags,
+                                            options=options,
+                                            strip_comment_tags=
+                                                strip_comment_tags):
+                        yield filename, lineno, message, comments
+                    break
+
+
+def extract_from_file(method, filename, keywords=DEFAULT_KEYWORDS,
+                      comment_tags=(), options=None, strip_comment_tags=False):
+    """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 dictionary mapping keywords (i.e. names of functions
+                     that should be recognized as translation functions) to
+                     tuples that specify which of their arguments contain
+                     localizable strings
+    :param comment_tags: a list of translator tags to search for and include
+                         in the results
+    :param strip_comment_tags: a flag that if set to `True` causes all comment
+                               tags to be removed from the collected comments.
+    :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, comment_tags, options,
+                            strip_comment_tags))
+    finally:
+        fileobj.close()
+
+
+def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, comment_tags=(),
+            options=None, strip_comment_tags=False):
+    """Extract messages from the given file-like object using the specified
+    extraction method.
+
+    This function returns a list of tuples of the form:
+
+        ``(lineno, message, comments)``
+
+    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, u'Hello, world!', [])
+
+    :param method: a string specifying the extraction method (.e.g. "python");
+                   if this is a simple name, the extraction function will be
+                   looked up by entry point; if it is an explicit reference
+                   to a function (of the form ``package.module:funcname`` or
+                   ``package.module.funcname``), the corresponding function
+                   will be imported and used
+    :param fileobj: the file-like object the messages should be extracted from
+    :param keywords: a dictionary mapping keywords (i.e. names of functions
+                     that should be recognized as translation functions) to
+                     tuples that specify which of their arguments contain
+                     localizable strings
+    :param comment_tags: a list of translator tags to search for and include
+                         in the results
+    :param options: a dictionary of additional options (optional)
+    :param strip_comment_tags: a flag that if set to `True` causes all comment
+                               tags to be removed from the collected comments.
+    :return: the list of extracted messages
+    :rtype: `list`
+    :raise ValueError: if the extraction method is not registered
+    """
+    func = None
+    if ':' in method or '.' in method:
+        if ':' not in method:
+            lastdot = method.rfind('.')
+            module, attrname = method[:lastdot], method[lastdot + 1:]
+        else:
+            module, attrname = method.split(':', 1)
+        func = getattr(__import__(module, {}, {}, [attrname]), attrname)
+    else:
+        try:
+            from pkg_resources import working_set
+        except ImportError:
+            # pkg_resources is not available, so we resort to looking up the
+            # builtin extractors directly
+            builtin = {'ignore': extract_nothing, 'python': extract_python}
+            func = builtin.get(method)
+        else:
+            for entry_point in working_set.iter_entry_points(GROUP_NAME,
+                                                             method):
+                func = entry_point.load(require=True)
+                break
+    if func is None:
+        raise ValueError('Unknown extraction method %r' % method)
+
+    results = func(fileobj, keywords.keys(), comment_tags,
+                   options=options or {})
+
+    for lineno, funcname, messages, comments in results:
+        if funcname:
+            spec = keywords[funcname] or (1,)
+        else:
+            spec = (1,)
+        if not isinstance(messages, (list, tuple)):
+            messages = [messages]
+        if not messages:
+            continue
+
+        # Validate the messages against the keyword's specification
+        msgs = []
+        invalid = False
+        # last_index is 1 based like the keyword spec
+        last_index = len(messages)
+        for index in spec:
+            if last_index < index:
+                # Not enough arguments
+                invalid = True
+                break
+            message = messages[index - 1]
+            if message is None:
+                invalid = True
+                break
+            msgs.append(message)
+        if invalid:
+            continue
+
+        first_msg_index = spec[0] - 1
+        if not messages[first_msg_index]:
+            # An empty string msgid isn't valid, emit a warning
+            where = '%s:%i' % (hasattr(fileobj, 'name') and \
+                                   fileobj.name or '(unknown)', lineno)
+            print >> sys.stderr, empty_msgid_warning % where
+            continue
+
+        messages = tuple(msgs)
+        if len(messages) == 1:
+            messages = messages[0]
+
+        if strip_comment_tags:
+            _strip_comment_tags(comments, comment_tags)
+        yield lineno, messages, comments
+
+
+def extract_nothing(fileobj, keywords, comment_tags, options):
+    """Pseudo extractor that does not actually extract anything, but simply
+    returns an empty list.
+    """
+    return []
+
+
+def extract_python(fileobj, keywords, comment_tags, options):
+    """Extract messages from Python source code.
+
+    :param fileobj: the seekable, 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 comment_tags: a list of translator tags to search for and include
+                         in the results
+    :param options: a dictionary of additional options (optional)
+    :return: an iterator over ``(lineno, funcname, message, comments)`` tuples
+    :rtype: ``iterator``
+    """
+    funcname = lineno = message_lineno = None
+    call_stack = -1
+    buf = []
+    messages = []
+    translator_comments = []
+    in_def = in_translator_comments = False
+    comment_tag = None
+
+    encoding = parse_encoding(fileobj) or options.get('encoding', 'iso-8859-1')
+
+    tokens = generate_tokens(fileobj.readline)
+    for tok, value, (lineno, _), _, _ in tokens:
+        if call_stack == -1 and tok == NAME and value in ('def', 'class'):
+            in_def = True
+        elif tok == OP and value == '(':
+            if in_def:
+                # Avoid false positives for declarations such as:
+                # def gettext(arg='message'):
+                in_def = False
+                continue
+            if funcname:
+                message_lineno = lineno
+                call_stack += 1
+        elif in_def and tok == OP and value == ':':
+            # End of a class definition without parens
+            in_def = False
+            continue
+        elif call_stack == -1 and tok == COMMENT:
+            # Strip the comment token from the line
+            value = value.decode(encoding)[1:].strip()
+            if in_translator_comments and \
+                    translator_comments[-1][0] == lineno - 1:
+                # We're already inside a translator comment, continue appending
+                translator_comments.append((lineno, value))
+                continue
+            # If execution reaches this point, let's see if comment line
+            # starts with one of the comment tags
+            for comment_tag in comment_tags:
+                if value.startswith(comment_tag):
+                    in_translator_comments = True
+                    translator_comments.append((lineno, value))
+                    break
+        elif funcname and call_stack == 0:
+            if tok == OP and value == ')':
+                if buf:
+                    messages.append(''.join(buf))
+                    del buf[:]
+                else:
+                    messages.append(None)
+
+                if len(messages) > 1:
+                    messages = tuple(messages)
+                else:
+                    messages = messages[0]
+                # Comments don't apply unless they immediately preceed the
+                # message
+                if translator_comments and \
+                        translator_comments[-1][0] < message_lineno - 1:
+                    translator_comments = []
+
+                yield (message_lineno, funcname, messages,
+                       [comment[1] for comment in translator_comments])
+
+                funcname = lineno = message_lineno = None
+                call_stack = -1
+                messages = []
+                translator_comments = []
+                in_translator_comments = False
+            elif tok == STRING:
+                # Unwrap quotes in a safe manner, maintaining the string's
+                # encoding
+                # https://sourceforge.net/tracker/?func=detail&atid=355470&
+                # aid=617979&group_id=5470
+                value = eval('# coding=%s\n%s' % (encoding, value),
+                             {'__builtins__':{}}, {})
+                if isinstance(value, str):
+                    value = value.decode(encoding)
+                buf.append(value)
+            elif tok == OP and value == ',':
+                if buf:
+                    messages.append(''.join(buf))
+                    del buf[:]
+                else:
+                    messages.append(None)
+                if translator_comments:
+                    # We have translator comments, and since we're on a
+                    # comma(,) user is allowed to break into a new line
+                    # Let's increase the last comment's lineno in order
+                    # for the comment to still be a valid one
+                    old_lineno, old_comment = translator_comments.pop()
+                    translator_comments.append((old_lineno+1, old_comment))
+        elif call_stack > 0 and tok == OP and value == ')':
+            call_stack -= 1
+        elif funcname and call_stack == -1:
+            funcname = None
+        elif tok == NAME and value in keywords:
+            funcname = value
+
+
+def extract_javascript(fileobj, keywords, comment_tags, options):
+    """Extract messages from JavaScript source code.
+
+    :param fileobj: the seekable, 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 comment_tags: a list of translator tags to search for and include
+                         in the results
+    :param options: a dictionary of additional options (optional)
+    :return: an iterator over ``(lineno, funcname, message, comments)`` tuples
+    :rtype: ``iterator``
+    """
+    from babel.messages.jslexer import tokenize, unquote_string
+    funcname = message_lineno = None
+    messages = []
+    last_argument = None
+    translator_comments = []
+    concatenate_next = False
+    encoding = options.get('encoding', 'utf-8')
+    last_token = None
+    call_stack = -1
+
+    for token in tokenize(fileobj.read().decode(encoding)):
+        if token.type == 'operator' and token.value == '(':
+            if funcname:
+                message_lineno = token.lineno
+                call_stack += 1
+
+        elif call_stack == -1 and token.type == 'linecomment':
+            value = token.value[2:].strip()
+            if translator_comments and \
+               translator_comments[-1][0] == token.lineno - 1:
+                translator_comments.append((token.lineno, value))
+                continue
+
+            for comment_tag in comment_tags:
+                if value.startswith(comment_tag):
+                    translator_comments.append((token.lineno, value.strip()))
+                    break
+
+        elif token.type == 'multilinecomment':
+            # only one multi-line comment may preceed a translation
+            translator_comments = []
+            value = token.value[2:-2].strip()
+            for comment_tag in comment_tags:
+                if value.startswith(comment_tag):
+                    lines = value.splitlines()
+                    if lines:
+                        lines[0] = lines[0].strip()
+                        lines[1:] = dedent('\n'.join(lines[1:])).splitlines()
+                        for offset, line in enumerate(lines):
+                            translator_comments.append((token.lineno + offset,
+                                                        line))
+                    break
+
+        elif funcname and call_stack == 0:
+            if token.type == 'operator' and token.value == ')':
+                if last_argument is not None:
+                    messages.append(last_argument)
+                if len(messages) > 1:
+                    messages = tuple(messages)
+                elif messages:
+                    messages = messages[0]
+                else:
+                    messages = None
+
+                # Comments don't apply unless they immediately precede the
+                # message
+                if translator_comments and \
+                   translator_comments[-1][0] < message_lineno - 1:
+                    translator_comments = []
+
+                if messages is not None:
+                    yield (message_lineno, funcname, messages,
+                           [comment[1] for comment in translator_comments])
+
+                funcname = message_lineno = last_argument = None
+                concatenate_next = False
+                translator_comments = []
+                messages = []
+                call_stack = -1
+
+            elif token.type == 'string':
+                new_value = unquote_string(token.value)
+                if concatenate_next:
+                    last_argument = (last_argument or '') + new_value
+                    concatenate_next = False
+                else:
+                    last_argument = new_value
+
+            elif token.type == 'operator':
+                if token.value == ',':
+                    if last_argument is not None:
+                        messages.append(last_argument)
+                        last_argument = None
+                    else:
+                        messages.append(None)
+                    concatenate_next = False
+                elif token.value == '+':
+                    concatenate_next = True
+
+        elif call_stack > 0 and token.type == 'operator' \
+             and token.value == ')':
+            call_stack -= 1
+
+        elif funcname and call_stack == -1:
+            funcname = None
+
+        elif call_stack == -1 and token.type == 'name' and \
+             token.value in keywords and \
+             (last_token is None or last_token.type != 'name' or
+              last_token.value != 'function'):
+            funcname = token.value
+
+        last_token = token
new file mode 100755
--- /dev/null
+++ b/babel3/babel/messages/frontend.py
@@ -0,0 +1,1201 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2008 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 ConfigParser import RawConfigParser
+from datetime import datetime
+from distutils import log
+from distutils.cmd import Command
+from distutils.errors import DistutilsOptionError, DistutilsSetupError
+from locale import getpreferredencoding
+import logging
+from optparse import OptionParser
+import os
+import re
+import shutil
+from StringIO import StringIO
+import sys
+import tempfile
+
+from babel import __version__ as VERSION
+from babel import Locale, localedata
+from babel.core import UnknownLocaleError
+from babel.messages.catalog import Catalog
+from babel.messages.extract import extract_from_dir, DEFAULT_KEYWORDS, \
+                                   DEFAULT_MAPPING
+from babel.messages.mofile import write_mo
+from babel.messages.pofile import read_po, write_po
+from babel.messages.plurals import PLURALS
+from babel.util import odict, LOCALTZ
+
+__all__ = ['CommandLineInterface', 'compile_catalog', 'extract_messages',
+           'init_catalog', 'check_message_extractors', 'update_catalog']
+__docformat__ = 'restructuredtext en'
+
+
+class compile_catalog(Command):
+    """Catalog compilation 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.messages.frontend import compile_catalog
+
+        setup(
+            ...
+            cmdclass = {'compile_catalog': compile_catalog}
+        )
+
+    :since: version 0.9
+    :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_
+    :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
+    """
+
+    description = 'compile message catalogs to binary MO files'
+    user_options = [
+        ('domain=', 'D',
+         "domain of PO file (default 'messages')"),
+        ('directory=', 'd',
+         'path to base directory containing the catalogs'),
+        ('input-file=', 'i',
+         'name of the input file'),
+        ('output-file=', 'o',
+         "name of the output file (default "
+         "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"),
+        ('locale=', 'l',
+         'locale of the catalog to compile'),
+        ('use-fuzzy', 'f',
+         'also include fuzzy translations'),
+        ('statistics', None,
+         'print statistics about translations')
+    ]
+    boolean_options = ['use-fuzzy', 'statistics']
+
+    def initialize_options(self):
+        self.domain = 'messages'
+        self.directory = None
+        self.input_file = None
+        self.output_file = None
+        self.locale = None
+        self.use_fuzzy = False
+        self.statistics = False
+
+    def finalize_options(self):
+        if not self.input_file and not self.directory:
+            raise DistutilsOptionError('you must specify either the input file '
+                                       'or the base directory')
+        if not self.output_file and not self.directory:
+            raise DistutilsOptionError('you must specify either the input file '
+                                       'or the base directory')
+
+    def run(self):
+        po_files = []
+        mo_files = []
+
+        if not self.input_file:
+            if self.locale:
+                po_files.append((self.locale,
+                                 os.path.join(self.directory, self.locale,
+                                              'LC_MESSAGES',
+                                              self.domain + '.po')))
+                mo_files.append(os.path.join(self.directory, self.locale,
+                                             'LC_MESSAGES',
+                                             self.domain + '.mo'))
+            else:
+                for locale in os.listdir(self.directory):
+                    po_file = os.path.join(self.directory, locale,
+                                           'LC_MESSAGES', self.domain + '.po')
+                    if os.path.exists(po_file):
+                        po_files.append((locale, po_file))
+                        mo_files.append(os.path.join(self.directory, locale,
+                                                     'LC_MESSAGES',
+                                                     self.domain + '.mo'))
+        else:
+            po_files.append((self.locale, self.input_file))
+            if self.output_file:
+                mo_files.append(self.output_file)
+            else:
+                mo_files.append(os.path.join(self.directory, self.locale,
+                                             'LC_MESSAGES',
+                                             self.domain + '.mo'))
+
+        if not po_files:
+            raise DistutilsOptionError('no message catalogs found')
+
+        for idx, (locale, po_file) in enumerate(po_files):
+            mo_file = mo_files[idx]
+            infile = open(po_file, 'r')
+            try:
+                catalog = read_po(infile, locale)
+            finally:
+                infile.close()
+
+            if self.statistics:
+                translated = 0
+                for message in list(catalog)[1:]:
+                    if message.string:
+                        translated +=1
+                percentage = 0
+                if len(catalog):
+                    percentage = translated * 100 // len(catalog)
+                log.info('%d of %d messages (%d%%) translated in %r',
+                         translated, len(catalog), percentage, po_file)
+
+            if catalog.fuzzy and not self.use_fuzzy:
+                log.warn('catalog %r is marked as fuzzy, skipping', po_file)
+                continue
+
+            for message, errors in catalog.check():
+                for error in errors:
+                    log.error('error: %s:%d: %s', po_file, message.lineno,
+                              error)
+
+            log.info('compiling catalog %r to %r', po_file, mo_file)
+
+            outfile = open(mo_file, 'wb')
+            try:
+                write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy)
+            finally:
+                outfile.close()
+
+
+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.messages.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',
+         'space-separated list of keywords to look for in addition to the '
+         'defaults'),
+        ('no-default-keywords', None,
+         'do not include the default keywords'),
+        ('mapping-file=', 'F',
+         'path to the mapping configuration file'),
+        ('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=', 'o',
+         'name of the output file'),
+        ('width=', 'w',
+         'set output line width (default 76)'),
+        ('no-wrap', None,
+         'do not break long message lines, longer than the output line width, '
+         'into several lines'),
+        ('sort-output', None,
+         'generate sorted output (default False)'),
+        ('sort-by-file', None,
+         'sort output by file location (default False)'),
+        ('msgid-bugs-address=', None,
+         'set report address for msgid'),
+        ('copyright-holder=', None,
+         'set copyright holder in output'),
+        ('add-comments=', 'c',
+         'place comment block with TAG (or those preceding keyword lines) in '
+         'output file. Seperate multiple TAGs with commas(,)'),
+        ('strip-comments', None,
+         'strip the comment TAGs from the comments.'),
+        ('input-dirs=', None,
+         'directories that should be scanned for messages'),
+    ]
+    boolean_options = [
+        'no-default-keywords', 'no-location', 'omit-header', 'no-wrap',
+        'sort-output', 'sort-by-file', 'strip-comments'
+    ]
+
+    def initialize_options(self):
+        self.charset = 'utf-8'
+        self.keywords = ''
+        self._keywords = DEFAULT_KEYWORDS.copy()
+        self.no_default_keywords = False
+        self.mapping_file = None
+        self.no_location = False
+        self.omit_header = False
+        self.output_file = None
+        self.input_dirs = None
+        self.width = None
+        self.no_wrap = False
+        self.sort_output = False
+        self.sort_by_file = False
+        self.msgid_bugs_address = None
+        self.copyright_holder = None
+        self.add_comments = None
+        self._add_comments = []
+        self.strip_comments = False
+
+    def finalize_options(self):
+        if self.no_default_keywords and not self.keywords:
+            raise DistutilsOptionError('you must specify new keywords if you '
+                                       'disable the default ones')
+        if self.no_default_keywords:
+            self._keywords = {}
+        if self.keywords:
+            self._keywords.update(parse_keywords(self.keywords.split()))
+
+        if not self.output_file:
+            raise DistutilsOptionError('no output file specified')
+        if self.no_wrap and self.width:
+            raise DistutilsOptionError("'--no-wrap' and '--width' are mutually "
+                                       "exclusive")
+        if not self.no_wrap and not self.width:
+            self.width = 76
+        elif self.width is not None:
+            self.width = int(self.width)
+
+        if self.sort_output and self.sort_by_file:
+            raise DistutilsOptionError("'--sort-output' and '--sort-by-file' "
+                                       "are mutually exclusive")
+
+        if not self.input_dirs:
+            self.input_dirs = dict.fromkeys([k.split('.',1)[0]
+                for k in self.distribution.packages
+            ]).keys()
+
+        if self.add_comments:
+            self._add_comments = self.add_comments.split(',')
+
+    def run(self):
+        mappings = self._get_mappings()
+        outfile = open(self.output_file, 'w')
+        try:
+            catalog = Catalog(project=self.distribution.get_name(),
+                              version=self.distribution.get_version(),
+                              msgid_bugs_address=self.msgid_bugs_address,
+                              copyright_holder=self.copyright_holder,
+                              charset=self.charset)
+
+            for dirname, (method_map, options_map) in mappings.items():
+                def callback(filename, method, options):
+                    if method == 'ignore':
+                        return
+                    filepath = os.path.normpath(os.path.join(dirname, filename))
+                    optstr = ''
+                    if options:
+                        optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for
+                                                      k, v in options.items()])
+                    log.info('extracting messages from %s%s', filepath, optstr)
+
+                extracted = extract_from_dir(dirname, method_map, options_map,
+                                             keywords=self._keywords,
+                                             comment_tags=self._add_comments,
+                                             callback=callback,
+                                             strip_comment_tags=
+                                                self.strip_comments)
+                for filename, lineno, message, comments in extracted:
+                    filepath = os.path.normpath(os.path.join(dirname, filename))
+                    catalog.add(message, None, [(filepath, lineno)],
+                                auto_comments=comments)
+
+            log.info('writing PO template file to %s' % self.output_file)
+            write_po(outfile, catalog, width=self.width,
+                     no_location=self.no_location,
+                     omit_header=self.omit_header,
+                     sort_output=self.sort_output,
+                     sort_by_file=self.sort_by_file)
+        finally:
+            outfile.close()
+
+    def _get_mappings(self):
+        mappings = {}
+
+        if self.mapping_file:
+            fileobj = open(self.mapping_file, 'U')
+            try:
+                method_map, options_map = parse_mapping(fileobj)
+                for dirname in self.input_dirs:
+                    mappings[dirname] = method_map, options_map
+            finally:
+                fileobj.close()
+
+        elif getattr(self.distribution, 'message_extractors', None):
+            message_extractors = self.distribution.message_extractors
+            for dirname, mapping in message_extractors.items():
+                if isinstance(mapping, basestring):
+                    method_map, options_map = parse_mapping(StringIO(mapping))
+                else:
+                    method_map, options_map = [], {}
+                    for pattern, method, options in mapping:
+                        method_map.append((pattern, method))
+                        options_map[pattern] = options or {}
+                mappings[dirname] = method_map, options_map
+
+        else:
+            for dirname in self.input_dirs:
+                mappings[dirname] = DEFAULT_MAPPING, {}
+
+        return mappings
+
+
+def check_message_extractors(dist, name, value):
+    """Validate the ``message_extractors`` keyword argument to ``setup()``.
+
+    :param dist: the distutils/setuptools ``Distribution`` object
+    :param name: the name of the keyword argument (should always be
+                 "message_extractors")
+    :param value: the value of the keyword argument
+    :raise `DistutilsSetupError`: if the value is not valid
+    :see: `Adding setup() arguments
+           <http://peak.telecommunity.com/DevCenter/setuptools#adding-setup-arguments>`_
+    """
+    assert name == 'message_extractors'
+    if not isinstance(value, dict):
+        raise DistutilsSetupError('the value of the "message_extractors" '
+                                  'parameter must be a dictionary')
+
+
+class init_catalog(Command):
+    """New catalog initialization 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.messages.frontend import init_catalog
+
+        setup(
+            ...
+            cmdclass = {'init_catalog': init_catalog}
+        )
+
+    :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_
+    :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
+    """
+
+    description = 'create a new catalog based on a POT file'
+    user_options = [
+        ('domain=', 'D',
+         "domain of PO file (default 'messages')"),
+        ('input-file=', 'i',
+         'name of the input file'),
+        ('output-dir=', 'd',
+         'path to output directory'),
+        ('output-file=', 'o',
+         "name of the output file (default "
+         "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"),
+        ('locale=', 'l',
+         'locale for the new localized catalog'),
+    ]
+
+    def initialize_options(self):
+        self.output_dir = None
+        self.output_file = None
+        self.input_file = None
+        self.locale = None
+        self.domain = 'messages'
+
+    def finalize_options(self):
+        if not self.input_file:
+            raise DistutilsOptionError('you must specify the input file')
+
+        if not self.locale:
+            raise DistutilsOptionError('you must provide a locale for the '
+                                       'new catalog')
+        try:
+            self._locale = Locale.parse(self.locale)
+        except UnknownLocaleError, e:
+            raise DistutilsOptionError(e)
+
+        if not self.output_file and not self.output_dir:
+            raise DistutilsOptionError('you must specify the output directory')
+        if not self.output_file:
+            self.output_file = os.path.join(self.output_dir, self.locale,
+                                            'LC_MESSAGES', self.domain + '.po')
+
+        if not os.path.exists(os.path.dirname(self.output_file)):
+            os.makedirs(os.path.dirname(self.output_file))
+
+    def run(self):
+        log.info('creating catalog %r based on %r', self.output_file,
+                 self.input_file)
+
+        infile = open(self.input_file, 'r')
+        try:
+            # Although reading from the catalog template, read_po must be fed
+            # the locale in order to correcly calculate plurals
+            catalog = read_po(infile, locale=self.locale)
+        finally:
+            infile.close()
+
+        catalog.locale = self._locale
+        catalog.fuzzy = False
+
+        outfile = open(self.output_file, 'w')
+        try:
+            write_po(outfile, catalog)
+        finally:
+            outfile.close()
+
+
+class update_catalog(Command):
+    """Catalog merging 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.messages.frontend import update_catalog
+
+        setup(
+            ...
+            cmdclass = {'update_catalog': update_catalog}
+        )
+
+    :since: version 0.9
+    :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_
+    :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
+    """
+
+    description = 'update message catalogs from a POT file'
+    user_options = [
+        ('domain=', 'D',
+         "domain of PO file (default 'messages')"),
+        ('input-file=', 'i',
+         'name of the input file'),
+        ('output-dir=', 'd',
+         'path to base directory containing the catalogs'),
+        ('output-file=', 'o',
+         "name of the output file (default "
+         "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"),
+        ('locale=', 'l',
+         'locale of the catalog to compile'),
+        ('ignore-obsolete=', None,
+         'whether to omit obsolete messages from the output'),
+        ('no-fuzzy-matching', 'N',
+         'do not use fuzzy matching'),
+        ('previous', None,
+         'keep previous msgids of translated messages')
+    ]
+    boolean_options = ['ignore_obsolete', 'no_fuzzy_matching', 'previous']
+
+    def initialize_options(self):
+        self.domain = 'messages'
+        self.input_file = None
+        self.output_dir = None
+        self.output_file = None
+        self.locale = None
+        self.ignore_obsolete = False
+        self.no_fuzzy_matching = False
+        self.previous = False
+
+    def finalize_options(self):
+        if not self.input_file:
+            raise DistutilsOptionError('you must specify the input file')
+        if not self.output_file and not self.output_dir:
+            raise DistutilsOptionError('you must specify the output file or '
+                                       'directory')
+        if self.output_file and not self.locale:
+            raise DistutilsOptionError('you must specify the locale')
+        if self.no_fuzzy_matching and self.previous:
+            self.previous = False
+
+    def run(self):
+        po_files = []
+        if not self.output_file:
+            if self.locale:
+                po_files.append((self.locale,
+                                 os.path.join(self.output_dir, self.locale,
+                                              'LC_MESSAGES',
+                                              self.domain + '.po')))
+            else:
+                for locale in os.listdir(self.output_dir):
+                    po_file = os.path.join(self.output_dir, locale,
+                                           'LC_MESSAGES',
+                                           self.domain + '.po')
+                    if os.path.exists(po_file):
+                        po_files.append((locale, po_file))
+        else:
+            po_files.append((self.locale, self.output_file))
+
+        domain = self.domain
+        if not domain:
+            domain = os.path.splitext(os.path.basename(self.input_file))[0]
+
+        infile = open(self.input_file, 'U')
+        try:
+            template = read_po(infile)
+        finally:
+            infile.close()
+
+        if not po_files:
+            raise DistutilsOptionError('no message catalogs found')
+
+        for locale, filename in po_files:
+            log.info('updating catalog %r based on %r', filename,
+                     self.input_file)
+            infile = open(filename, 'U')
+            try:
+                catalog = read_po(infile, locale=locale, domain=domain)
+            finally:
+                infile.close()
+
+            catalog.update(template, self.no_fuzzy_matching)
+
+            tmpname = os.path.join(os.path.dirname(filename),
+                                   tempfile.gettempprefix() +
+                                   os.path.basename(filename))
+            tmpfile = open(tmpname, 'w')
+            try:
+                try:
+                    write_po(tmpfile, catalog,
+                             ignore_obsolete=self.ignore_obsolete,
+                             include_previous=self.previous)
+                finally:
+                    tmpfile.close()
+            except:
+                os.remove(tmpname)
+                raise
+
+            try:
+                os.rename(tmpname, filename)
+            except OSError:
+                # We're probably on Windows, which doesn't support atomic
+                # renames, at least not through Python
+                # If the error is in fact due to a permissions problem, that
+                # same error is going to be raised from one of the following
+                # operations
+                os.remove(filename)
+                shutil.copy(tmpname, filename)
+                os.remove(tmpname)
+
+
+class CommandLineInterface(object):
+    """Command-line interface.
+
+    This class provides a simple command-line interface to the message
+    extraction and PO file generation functionality.
+    """
+
+    usage = '%%prog %s [options] %s'
+    version = '%%prog %s' % VERSION
+    commands = {
+        'compile': 'compile message catalogs to MO files',
+        'extract': 'extract messages from source files and generate a POT file',
+        'init':    'create new message catalogs from a POT file',
+        'update':  'update existing message catalogs from a POT file'
+    }
+
+    def run(self, argv=sys.argv):
+        """Main entry point of the command-line interface.
+
+        :param argv: list of arguments passed on the command-line
+        """
+        self.parser = OptionParser(usage=self.usage % ('command', '[args]'),
+                                   version=self.version)
+        self.parser.disable_interspersed_args()
+        self.parser.print_help = self._help
+        self.parser.add_option('--list-locales', dest='list_locales',
+                               action='store_true',
+                               help="print all known locales and exit")
+        self.parser.add_option('-v', '--verbose', action='store_const',
+                               dest='loglevel', const=logging.DEBUG,
+                               help='print as much as possible')
+        self.parser.add_option('-q', '--quiet', action='store_const',
+                               dest='loglevel', const=logging.ERROR,
+                               help='print as little as possible')
+        self.parser.set_defaults(list_locales=False, loglevel=logging.INFO)
+
+        options, args = self.parser.parse_args(argv[1:])
+
+        # Configure logging
+        self.log = logging.getLogger('babel')
+        self.log.setLevel(options.loglevel)
+        handler = logging.StreamHandler()
+        handler.setLevel(options.loglevel)
+        formatter = logging.Formatter('%(message)s')
+        handler.setFormatter(formatter)
+        self.log.addHandler(handler)
+
+        if options.list_locales:
+            identifiers = localedata.list()
+            longest = max([len(identifier) for identifier in identifiers])
+            identifiers.sort()
+            format = u'%%-%ds %%s' % (longest + 1)
+            for identifier in identifiers:
+                locale = Locale.parse(identifier)
+                output = format % (identifier, locale.english_name)
+                print output.encode(sys.stdout.encoding or
+                                    getpreferredencoding() or
+                                    'ascii', 'replace')
+            return 0
+
+        if not args:
+            self.parser.error('no valid command or option passed. '
+                              'Try the -h/--help option for more information.')
+
+        cmdname = args[0]
+        if cmdname not in self.commands:
+            self.parser.error('unknown command "%s"' % cmdname)
+
+        return getattr(self, cmdname)(args[1:])
+
+    def _help(self):
+        print self.parser.format_help()
+        print "commands:"
+        longest = max([len(command) for command in self.commands])
+        format = "  %%-%ds %%s" % max(8, longest + 1)
+        commands = self.commands.items()
+        commands.sort()
+        for name, description in commands:
+            print format % (name, description)
+
+    def compile(self, argv):
+        """Subcommand for compiling a message catalog to a MO file.
+
+        :param argv: the command arguments
+        :since: version 0.9
+        """
+        parser = OptionParser(usage=self.usage % ('compile', ''),
+                              description=self.commands['compile'])
+        parser.add_option('--domain', '-D', dest='domain',
+                          help="domain of MO and PO files (default '%default')")
+        parser.add_option('--directory', '-d', dest='directory',
+                          metavar='DIR', help='base directory of catalog files')
+        parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE',
+                          help='locale of the catalog')
+        parser.add_option('--input-file', '-i', dest='input_file',
+                          metavar='FILE', help='name of the input file')
+        parser.add_option('--output-file', '-o', dest='output_file',
+                          metavar='FILE',
+                          help="name of the output file (default "
+                               "'<output_dir>/<locale>/LC_MESSAGES/"
+                               "<domain>.mo')")
+        parser.add_option('--use-fuzzy', '-f', dest='use_fuzzy',
+                          action='store_true',
+                          help='also include fuzzy translations (default '
+                               '%default)')
+        parser.add_option('--statistics', dest='statistics',
+                          action='store_true',
+                          help='print statistics about translations')
+
+        parser.set_defaults(domain='messages', use_fuzzy=False,
+                            compile_all=False, statistics=False)
+        options, args = parser.parse_args(argv)
+
+        po_files = []
+        mo_files = []
+        if not options.input_file:
+            if not options.directory:
+                parser.error('you must specify either the input file or the '
+                             'base directory')
+            if options.locale:
+                po_files.append((options.locale,
+                                 os.path.join(options.directory,
+                                              options.locale, 'LC_MESSAGES',
+                                              options.domain + '.po')))
+                mo_files.append(os.path.join(options.directory, options.locale,
+                                             'LC_MESSAGES',
+                                             options.domain + '.mo'))
+            else:
+                for locale in os.listdir(options.directory):
+                    po_file = os.path.join(options.directory, locale,
+                                           'LC_MESSAGES', options.domain + '.po')
+                    if os.path.exists(po_file):
+                        po_files.append((locale, po_file))
+                        mo_files.append(os.path.join(options.directory, locale,
+                                                     'LC_MESSAGES',
+                                                     options.domain + '.mo'))
+        else:
+            po_files.append((options.locale, options.input_file))
+            if options.output_file:
+                mo_files.append(options.output_file)
+            else:
+                if not options.directory:
+                    parser.error('you must specify either the input file or '
+                                 'the base directory')
+                mo_files.append(os.path.join(options.directory, options.locale,
+                                             'LC_MESSAGES',
+                                             options.domain + '.mo'))
+        if not po_files:
+            parser.error('no message catalogs found')
+
+        for idx, (locale, po_file) in enumerate(po_files):
+            mo_file = mo_files[idx]
+            infile = open(po_file, 'r')
+            try:
+                catalog = read_po(infile, locale)
+            finally:
+                infile.close()
+
+            if options.statistics:
+                translated = 0
+                for message in list(catalog)[1:]:
+                    if message.string:
+                        translated +=1
+                percentage = 0
+                if len(catalog):
+                    percentage = translated * 100 // len(catalog)
+                self.log.info("%d of %d messages (%d%%) translated in %r",
+                              translated, len(catalog), percentage, po_file)
+
+            if catalog.fuzzy and not options.use_fuzzy:
+                self.log.warn('catalog %r is marked as fuzzy, skipping',
+                              po_file)
+                continue
+
+            for message, errors in catalog.check():
+                for error in errors:
+                    self.log.error('error: %s:%d: %s', po_file, message.lineno,
+                                   error)
+
+            self.log.info('compiling catalog %r to %r', po_file, mo_file)
+
+            outfile = open(mo_file, 'wb')
+            try:
+                write_mo(outfile, catalog, use_fuzzy=options.use_fuzzy)
+            finally:
+                outfile.close()
+
+    def extract(self, argv):
+        """Subcommand for extracting messages from source files and generating
+        a POT file.
+
+        :param argv: the command arguments
+        """
+        parser = OptionParser(usage=self.usage % ('extract', 'dir1 <dir2> ...'),
+                              description=self.commands['extract'])
+        parser.add_option('--charset', dest='charset',
+                          help='charset to use in the output (default '
+                               '"%default")')
+        parser.add_option('-k', '--keyword', dest='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-default-keywords', dest='no_default_keywords',
+                          action='store_true',
+                          help="do not include the default keywords")
+        parser.add_option('--mapping', '-F', dest='mapping_file',
+                          help='path to the extraction mapping file')
+        parser.add_option('--no-location', dest='no_location',
+                          action='store_true',
+                          help='do not include location comments with filename '
+                               'and line number')
+        parser.add_option('--omit-header', dest='omit_header',
+                          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')
+        parser.add_option('-w', '--width', dest='width', type='int',
+                          help="set output line width (default 76)")
+        parser.add_option('--no-wrap', dest='no_wrap', action = 'store_true',
+                          help='do not break long message lines, longer than '
+                               'the output line width, into several lines')
+        parser.add_option('--sort-output', dest='sort_output',
+                          action='store_true',
+                          help='generate sorted output (default False)')
+        parser.add_option('--sort-by-file', dest='sort_by_file',
+                          action='store_true',
+                          help='sort output by file location (default False)')
+        parser.add_option('--msgid-bugs-address', dest='msgid_bugs_address',
+                          metavar='EMAIL@ADDRESS',
+                          help='set report address for msgid')
+        parser.add_option('--copyright-holder', dest='copyright_holder',
+                          help='set copyright holder in output')
+        parser.add_option('--project', dest='project',
+                          help='set project name in output')
+        parser.add_option('--version', dest='version',
+                          help='set project version in output')
+        parser.add_option('--add-comments', '-c', dest='comment_tags',
+                          metavar='TAG', action='append',
+                          help='place comment block with TAG (or those '
+                               'preceding keyword lines) in output file. One '
+                               'TAG per argument call')
+        parser.add_option('--strip-comment-tags', '-s',
+                          dest='strip_comment_tags', action='store_true',
+                          help='Strip the comment tags from the comments.')
+
+        parser.set_defaults(charset='utf-8', keywords=[],
+                            no_default_keywords=False, no_location=False,
+                            omit_header = False, width=None, no_wrap=False,
+                            sort_output=False, sort_by_file=False,
+                            comment_tags=[], strip_comment_tags=False)
+        options, args = parser.parse_args(argv)
+        if not args:
+            parser.error('incorrect number of arguments')
+
+        if options.output not in (None, '-'):
+            outfile = open(options.output, 'w')
+        else:
+            outfile = sys.stdout
+
+        keywords = DEFAULT_KEYWORDS.copy()
+        if options.no_default_keywords:
+            if not options.keywords:
+                parser.error('you must specify new keywords if you disable the '
+                             'default ones')
+            keywords = {}
+        if options.keywords:
+            keywords.update(parse_keywords(options.keywords))
+
+        if options.mapping_file:
+            fileobj = open(options.mapping_file, 'U')
+            try:
+                method_map, options_map = parse_mapping(fileobj)
+            finally:
+                fileobj.close()
+        else:
+            method_map = DEFAULT_MAPPING
+            options_map = {}
+
+        if options.width and options.no_wrap:
+            parser.error("'--no-wrap' and '--width' are mutually exclusive.")
+        elif not options.width and not options.no_wrap:
+            options.width = 76
+
+        if options.sort_output and options.sort_by_file:
+            parser.error("'--sort-output' and '--sort-by-file' are mutually "
+                         "exclusive")
+
+        try:
+            catalog = Catalog(project=options.project,
+                              version=options.version,
+                              msgid_bugs_address=options.msgid_bugs_address,
+                              copyright_holder=options.copyright_holder,
+                              charset=options.charset)
+
+            for dirname in args:
+                if not os.path.isdir(dirname):
+                    parser.error('%r is not a directory' % dirname)
+
+                def callback(filename, method, options):
+                    if method == 'ignore':
+                        return
+                    filepath = os.path.normpath(os.path.join(dirname, filename))
+                    optstr = ''
+                    if options:
+                        optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for
+                                                      k, v in options.items()])
+                    self.log.info('extracting messages from %s%s', filepath,
+                                  optstr)
+
+                extracted = extract_from_dir(dirname, method_map, options_map,
+                                             keywords, options.comment_tags,
+                                             callback=callback,
+                                             strip_comment_tags=
+                                                options.strip_comment_tags)
+                for filename, lineno, message, comments in extracted:
+                    filepath = os.path.normpath(os.path.join(dirname, filename))
+                    catalog.add(message, None, [(filepath, lineno)],
+                                auto_comments=comments)
+
+            if options.output not in (None, '-'):
+                self.log.info('writing PO template file to %s' % options.output)
+            write_po(outfile, catalog, width=options.width,
+                     no_location=options.no_location,
+                     omit_header=options.omit_header,
+                     sort_output=options.sort_output,
+                     sort_by_file=options.sort_by_file)
+        finally:
+            if options.output:
+                outfile.close()
+
+    def init(self, argv):
+        """Subcommand for creating new message catalogs from a template.
+
+        :param argv: the command arguments
+        """
+        parser = OptionParser(usage=self.usage % ('init', ''),
+                              description=self.commands['init'])
+        parser.add_option('--domain', '-D', dest='domain',
+                          help="domain of PO file (default '%default')")
+        parser.add_option('--input-file', '-i', dest='input_file',
+                          metavar='FILE', help='name of the input file')
+        parser.add_option('--output-dir', '-d', dest='output_dir',
+                          metavar='DIR', help='path to output directory')
+        parser.add_option('--output-file', '-o', dest='output_file',
+                          metavar='FILE',
+                          help="name of the output file (default "
+                               "'<output_dir>/<locale>/LC_MESSAGES/"
+                               "<domain>.po')")
+        parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE',
+                          help='locale for the new localized catalog')
+
+        parser.set_defaults(domain='messages')
+        options, args = parser.parse_args(argv)
+
+        if not options.locale:
+            parser.error('you must provide a locale for the new catalog')
+        try:
+            locale = Locale.parse(options.locale)
+        except UnknownLocaleError, e:
+            parser.error(e)
+
+        if not options.input_file:
+            parser.error('you must specify the input file')
+
+        if not options.output_file and not options.output_dir:
+            parser.error('you must specify the output file or directory')
+
+        if not options.output_file:
+            options.output_file = os.path.join(options.output_dir,
+                                               options.locale, 'LC_MESSAGES',
+                                               options.domain + '.po')
+        if not os.path.exists(os.path.dirname(options.output_file)):
+            os.makedirs(os.path.dirname(options.output_file))
+
+        infile = open(options.input_file, 'r')
+        try:
+            # Although reading from the catalog template, read_po must be fed
+            # the locale in order to correcly calculate plurals
+            catalog = read_po(infile, locale=options.locale)
+        finally:
+            infile.close()
+
+        catalog.locale = locale
+        catalog.revision_date = datetime.now(LOCALTZ)
+
+        self.log.info('creating catalog %r based on %r', options.output_file,
+                      options.input_file)
+
+        outfile = open(options.output_file, 'w')
+        try:
+            write_po(outfile, catalog)
+        finally:
+            outfile.close()
+
+    def update(self, argv):
+        """Subcommand for updating existing message catalogs from a template.
+
+        :param argv: the command arguments
+        :since: version 0.9
+        """
+        parser = OptionParser(usage=self.usage % ('update', ''),
+                              description=self.commands['update'])
+        parser.add_option('--domain', '-D', dest='domain',
+                          help="domain of PO file (default '%default')")
+        parser.add_option('--input-file', '-i', dest='input_file',
+                          metavar='FILE', help='name of the input file')
+        parser.add_option('--output-dir', '-d', dest='output_dir',
+                          metavar='DIR', help='path to output directory')
+        parser.add_option('--output-file', '-o', dest='output_file',
+                          metavar='FILE',
+                          help="name of the output file (default "
+                               "'<output_dir>/<locale>/LC_MESSAGES/"
+                               "<domain>.po')")
+        parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE',
+                          help='locale of the translations catalog')
+        parser.add_option('--ignore-obsolete', dest='ignore_obsolete',
+                          action='store_true',
+                          help='do not include obsolete messages in the output '
+                               '(default %default)')
+        parser.add_option('--no-fuzzy-matching', '-N', dest='no_fuzzy_matching',
+                          action='store_true',
+                          help='do not use fuzzy matching (default %default)')
+        parser.add_option('--previous', dest='previous', action='store_true',
+                          help='keep previous msgids of translated messages '
+                               '(default %default)')
+
+        parser.set_defaults(domain='messages', ignore_obsolete=False,
+                            no_fuzzy_matching=False, previous=False)
+        options, args = parser.parse_args(argv)
+
+        if not options.input_file:
+            parser.error('you must specify the input file')
+        if not options.output_file and not options.output_dir:
+            parser.error('you must specify the output file or directory')
+        if options.output_file and not options.locale:
+            parser.error('you must specify the locale')
+        if options.no_fuzzy_matching and options.previous:
+            options.previous = False
+
+        po_files = []
+        if not options.output_file:
+            if options.locale:
+                po_files.append((options.locale,
+                                 os.path.join(options.output_dir,
+                                              options.locale, 'LC_MESSAGES',
+                                              options.domain + '.po')))
+            else:
+                for locale in os.listdir(options.output_dir):
+                    po_file = os.path.join(options.output_dir, locale,
+                                           'LC_MESSAGES',
+                                           options.domain + '.po')
+                    if os.path.exists(po_file):
+                        po_files.append((locale, po_file))
+        else:
+            po_files.append((options.locale, options.output_file))
+
+        domain = options.domain
+        if not domain:
+            domain = os.path.splitext(os.path.basename(options.input_file))[0]
+
+        infile = open(options.input_file, 'U')
+        try:
+            template = read_po(infile)
+        finally:
+            infile.close()
+
+        if not po_files:
+            parser.error('no message catalogs found')
+
+        for locale, filename in po_files:
+            self.log.info('updating catalog %r based on %r', filename,
+                          options.input_file)
+            infile = open(filename, 'U')
+            try:
+                catalog = read_po(infile, locale=locale, domain=domain)
+            finally:
+                infile.close()
+
+            catalog.update(template, options.no_fuzzy_matching)
+
+            tmpname = os.path.join(os.path.dirname(filename),
+                                   tempfile.gettempprefix() +
+                                   os.path.basename(filename))
+            tmpfile = open(tmpname, 'w')
+            try:
+                try:
+                    write_po(tmpfile, catalog,
+                             ignore_obsolete=options.ignore_obsolete,
+                             include_previous=options.previous)
+                finally:
+                    tmpfile.close()
+            except:
+                os.remove(tmpname)
+                raise
+
+            try:
+                os.rename(tmpname, filename)
+            except OSError:
+                # We're probably on Windows, which doesn't support atomic
+                # renames, at least not through Python
+                # If the error is in fact due to a permissions problem, that
+                # same error is going to be raised from one of the following
+                # operations
+                os.remove(filename)
+                shutil.copy(tmpname, filename)
+                os.remove(tmpname)
+
+
+def main():
+    return CommandLineInterface().run(sys.argv)
+
+def parse_mapping(fileobj, filename=None):
+    """Parse an extraction method mapping from a file-like object.
+
+    >>> buf = StringIO('''
+    ... [extractors]
+    ... custom = mypackage.module:myfunc
+    ... 
+    ... # Python source files
+    ... [python: **.py]
+    ...
+    ... # Genshi templates
+    ... [genshi: **/templates/**.html]
+    ... include_attrs =
+    ... [genshi: **/templates/**.txt]
+    ... template_class = genshi.template:TextTemplate
+    ... encoding = latin-1
+    ... 
+    ... # Some custom extractor
+    ... [custom: **/custom/*.*]
+    ... ''')
+
+    >>> method_map, options_map = parse_mapping(buf)
+    >>> len(method_map)
+    4
+
+    >>> method_map[0]
+    ('**.py', 'python')
+    >>> options_map['**.py']
+    {}
+    >>> method_map[1]
+    ('**/templates/**.html', 'genshi')
+    >>> options_map['**/templates/**.html']['include_attrs']
+    ''
+    >>> method_map[2]
+    ('**/templates/**.txt', 'genshi')
+    >>> options_map['**/templates/**.txt']['template_class']
+    'genshi.template:TextTemplate'
+    >>> options_map['**/templates/**.txt']['encoding']
+    'latin-1'
+
+    >>> method_map[3]
+    ('**/custom/*.*', 'mypackage.module:myfunc')
+    >>> options_map['**/custom/*.*']
+    {}
+
+    :param fileobj: a readable file-like object containing the configuration
+                    text to parse
+    :return: a `(method_map, options_map)` tuple
+    :rtype: `tuple`
+    :see: `extract_from_directory`
+    """
+    extractors = {}
+    method_map = []
+    options_map = {}
+
+    parser = RawConfigParser()
+    parser._sections = odict(parser._sections) # We need ordered sections
+    parser.readfp(fileobj, filename)
+    for section in parser.sections():
+        if section == 'extractors':
+            extractors = dict(parser.items(section))
+        else:
+            method, pattern = [part.strip() for part in section.split(':', 1)]
+            method_map.append((pattern, method))
+            options_map[pattern] = dict(parser.items(section))
+
+    if extractors:
+        for idx, (pattern, method) in enumerate(method_map):
+            if method in extractors:
+                method = extractors[method]
+            method_map[idx] = (pattern, method)
+
+    return (method_map, options_map)
+
+def parse_keywords(strings=[]):
+    """Parse keywords specifications from the given list of strings.
+
+    >>> kw = parse_keywords(['_', 'dgettext:2', 'dngettext:2,3']).items()
+    >>> kw.sort()
+    >>> for keyword, indices in kw:
+    ...     print (keyword, indices)
+    ('_', None)
+    ('dgettext', (2,))
+    ('dngettext', (2, 3))
+    """
+    keywords = {}
+    for string in strings:
+        if ':' in string:
+            funcname, indices = string.split(':')
+        else:
+            funcname, indices = string, None
+        if funcname not in keywords:
+            if indices:
+                indices = tuple([(int(x)) for x in indices.split(',')])
+            keywords[funcname] = indices
+    return keywords
+
+
+if __name__ == '__main__':
+    main()
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/jslexer.py
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008 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/.
+
+"""A simple JavaScript 1.5 lexer which is used for the JavaScript
+extractor.
+"""
+
+import re
+
+from babel.util import itemgetter
+
+
+operators = [
+    '+', '-', '*', '%', '!=', '==', '<', '>', '<=', '>=', '=',
+    '+=', '-=', '*=', '%=', '<<', '>>', '>>>', '<<=', '>>=',
+    '>>>=', '&', '&=', '|', '|=', '&&', '||', '^', '^=', '(', ')',
+    '[', ']', '{', '}', '!', '--', '++', '~', ',', ';', '.', ':'
+]
+operators.sort(lambda a, b: cmp(-len(a), -len(b)))
+
+escapes = {'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t'}
+
+rules = [
+    (None, re.compile(r'\s+(?u)')),
+    (None, re.compile(r'<!--.*')),
+    ('linecomment', re.compile(r'//.*')),
+    ('multilinecomment', re.compile(r'/\*.*?\*/(?us)')),
+    ('name', re.compile(r'(\$+\w*|[^\W\d]\w*)(?u)')),
+    ('number', re.compile(r'''(?x)(
+        (?:0|[1-9]\d*)
+        (\.\d+)?
+        ([eE][-+]?\d+)? |
+        (0x[a-fA-F0-9]+)
+    )''')),
+    ('operator', re.compile(r'(%s)' % '|'.join(map(re.escape, operators)))),
+    ('string', re.compile(r'''(?xs)(
+        '(?:[^'\\]*(?:\\.[^'\\]*)*)'  |
+        "(?:[^"\\]*(?:\\.[^"\\]*)*)"
+    )'''))
+]
+
+division_re = re.compile(r'/=?')
+regex_re = re.compile(r'/(?:[^/\\]*(?:\\.[^/\\]*)*)/[a-zA-Z]*(?s)')
+line_re = re.compile(r'(\r\n|\n|\r)')
+line_join_re = re.compile(r'\\' + line_re.pattern)
+uni_escape_re = re.compile(r'[a-fA-F0-9]{1,4}')
+
+
+class Token(tuple):
+    """Represents a token as returned by `tokenize`."""
+    __slots__ = ()
+
+    def __new__(cls, type, value, lineno):
+        return tuple.__new__(cls, (type, value, lineno))
+
+    type = property(itemgetter(0))
+    value = property(itemgetter(1))
+    lineno = property(itemgetter(2))
+
+
+def indicates_division(token):
+    """A helper function that helps the tokenizer to decide if the current
+    token may be followed by a division operator.
+    """
+    if token.type == 'operator':
+        return token.value in (')', ']', '}', '++', '--')
+    return token.type in ('name', 'number', 'string', 'regexp')
+
+
+def unquote_string(string):
+    """Unquote a string with JavaScript rules.  The string has to start with
+    string delimiters (``'`` or ``"``.)
+
+    :return: a string
+    """
+    assert string and string[0] == string[-1] and string[0] in '"\'', \
+        'string provided is not properly delimited'
+    string = line_join_re.sub('\\1', string[1:-1])
+    result = []
+    add = result.append
+    pos = 0
+
+    while 1:
+        # scan for the next escape
+        escape_pos = string.find('\\', pos)
+        if escape_pos < 0:
+            break
+        add(string[pos:escape_pos])
+
+        # check which character is escaped
+        next_char = string[escape_pos + 1]
+        if next_char in escapes:
+            add(escapes[next_char])
+
+        # unicode escapes.  trie to consume up to four characters of
+        # hexadecimal characters and try to interpret them as unicode
+        # character point.  If there is no such character point, put
+        # all the consumed characters into the string.
+        elif next_char in 'uU':
+            escaped = uni_escape_re.match(string, escape_pos + 2)
+            if escaped is not None:
+                escaped_value = escaped.group()
+                if len(escaped_value) == 4:
+                    try:
+                        add(unichr(int(escaped_value, 16)))
+                    except ValueError:
+                        pass
+                    else:
+                        pos = escape_pos + 6
+                        continue
+                add(next_char + escaped_value)
+                pos = escaped.end()
+                continue
+            else:
+                add(next_char)
+
+        # bogus escape.  Just remove the backslash.
+        else:
+            add(next_char)
+        pos = escape_pos + 2
+
+    if pos < len(string):
+        add(string[pos:])
+
+    return u''.join(result)
+
+
+def tokenize(source):
+    """Tokenize a JavaScript source.
+
+    :return: generator of `Token`\s
+    """
+    may_divide = False
+    pos = 0
+    lineno = 1
+    end = len(source)
+
+    while pos < end:
+        # handle regular rules first
+        for token_type, rule in rules:
+            match = rule.match(source, pos)
+            if match is not None:
+                break
+        # if we don't have a match we don't give up yet, but check for
+        # division operators or regular expression literals, based on
+        # the status of `may_divide` which is determined by the last
+        # processed non-whitespace token using `indicates_division`.
+        else:
+            if may_divide:
+                match = division_re.match(source, pos)
+                token_type = 'operator'
+            else:
+                match = regex_re.match(source, pos)
+                token_type = 'regexp'
+            if match is None:
+                # woops. invalid syntax. jump one char ahead and try again.
+                pos += 1
+                continue
+
+        token_value = match.group()
+        if token_type is not None:
+            token = Token(token_type, token_value, lineno)
+            may_divide = indicates_division(token)
+            yield token
+        lineno += len(line_re.findall(token_value))
+        pos = match.end()
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/mofile.py
@@ -0,0 +1,216 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2008 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/.
+
+"""Writing of files in the ``gettext`` MO (machine object) format.
+
+:since: version 0.9
+:see: `The Format of MO Files
+       <http://www.gnu.org/software/gettext/manual/gettext.html#MO-Files>`_
+"""
+
+import array
+import struct
+
+from babel.messages.catalog import Catalog, Message
+
+__all__ = ['read_mo', 'write_mo']
+__docformat__ = 'restructuredtext en'
+
+
+LE_MAGIC = 0x950412deL
+BE_MAGIC = 0xde120495L
+
+def read_mo(fileobj):
+    """Read a binary MO file from the given file-like object and return a
+    corresponding `Catalog` object.
+    
+    :param fileobj: the file-like object to read the MO file from
+    :return: a catalog object representing the parsed MO file
+    :rtype: `Catalog`
+    
+    :note: The implementation of this function is heavily based on the
+           ``GNUTranslations._parse`` method of the ``gettext`` module in the
+           standard library.
+    """
+    catalog = Catalog()
+    headers = {}
+
+    filename = getattr(fileobj, 'name', '')
+    charset = None
+
+    buf = fileobj.read()
+    buflen = len(buf)
+    unpack = struct.unpack
+
+    # Parse the .mo file header, which consists of 5 little endian 32
+    # bit words.
+    magic = unpack('<I', buf[:4])[0] # Are we big endian or little endian?
+    if magic == LE_MAGIC:
+        version, msgcount, origidx, transidx = unpack('<4I', buf[4:20])
+        ii = '<II'
+    elif magic == BE_MAGIC:
+        version, msgcount, origidx, transidx = unpack('>4I', buf[4:20])
+        ii = '>II'
+    else:
+        raise IOError(0, 'Bad magic number', filename)
+
+    # Now put all messages from the .mo file buffer into the catalog
+    # dictionary
+    for i in xrange(0, msgcount):
+        mlen, moff = unpack(ii, buf[origidx:origidx + 8])
+        mend = moff + mlen
+        tlen, toff = unpack(ii, buf[transidx:transidx + 8])
+        tend = toff + tlen
+        if mend < buflen and tend < buflen:
+            msg = buf[moff:mend]
+            tmsg = buf[toff:tend]
+        else:
+            raise IOError(0, 'File is corrupt', filename)
+
+        # See if we're looking at GNU .mo conventions for metadata
+        if mlen == 0:
+            # Catalog description
+            lastkey = key = None
+            for item in tmsg.splitlines():
+                item = item.strip()
+                if not item:
+                    continue
+                if ':' in item:
+                    key, value = item.split(':', 1)
+                    lastkey = key = key.strip().lower()
+                    headers[key] = value.strip()
+                elif lastkey:
+                    headers[lastkey] += '\n' + item
+
+        if '\x04' in msg: # context
+            ctxt, msg = msg.split('\x04')
+        else:
+            ctxt = None
+
+        if '\x00' in msg: # plural forms
+            msg = msg.split('\x00')
+            tmsg = tmsg.split('\x00')
+            if catalog.charset:
+                msg = [x.decode(catalog.charset) for x in msg]
+                tmsg = [x.decode(catalog.charset) for x in tmsg]
+        else:
+            if catalog.charset:
+                msg = msg.decode(catalog.charset)
+                tmsg = tmsg.decode(catalog.charset)
+        catalog[msg] = Message(msg, tmsg, context=ctxt)
+
+        # advance to next entry in the seek tables
+        origidx += 8
+        transidx += 8
+
+    catalog.mime_headers = headers.items()
+    return catalog
+
+def write_mo(fileobj, catalog, use_fuzzy=False):
+    """Write a catalog to the specified file-like object using the GNU MO file
+    format.
+    
+    >>> from babel.messages import Catalog
+    >>> from gettext import GNUTranslations
+    >>> from StringIO import StringIO
+    
+    >>> catalog = Catalog(locale='en_US')
+    >>> catalog.add('foo', 'Voh')
+    >>> catalog.add((u'bar', u'baz'), (u'Bahr', u'Batz'))
+    >>> catalog.add('fuz', 'Futz', flags=['fuzzy'])
+    >>> catalog.add('Fizz', '')
+    >>> catalog.add(('Fuzz', 'Fuzzes'), ('', ''))
+    >>> buf = StringIO()
+    
+    >>> write_mo(buf, catalog)
+    >>> buf.seek(0)
+    >>> translations = GNUTranslations(fp=buf)
+    >>> translations.ugettext('foo')
+    u'Voh'
+    >>> translations.ungettext('bar', 'baz', 1)
+    u'Bahr'
+    >>> translations.ungettext('bar', 'baz', 2)
+    u'Batz'
+    >>> translations.ugettext('fuz')
+    u'fuz'
+    >>> translations.ugettext('Fizz')
+    u'Fizz'
+    >>> translations.ugettext('Fuzz')
+    u'Fuzz'
+    >>> translations.ugettext('Fuzzes')
+    u'Fuzzes'
+    
+    :param fileobj: the file-like object to write to
+    :param catalog: the `Catalog` instance
+    :param use_fuzzy: whether translations marked as "fuzzy" should be included
+                      in the output
+    """
+    messages = list(catalog)
+    if not use_fuzzy:
+        messages[1:] = [m for m in messages[1:] if not m.fuzzy]
+    messages.sort()
+
+    ids = strs = ''
+    offsets = []
+
+    for message in messages:
+        # For each string, we need size and file offset.  Each string is NUL
+        # terminated; the NUL does not count into the size.
+        if message.pluralizable:
+            msgid = '\x00'.join([
+                msgid.encode(catalog.charset) for msgid in message.id
+            ])
+            msgstrs = []
+            for idx, string in enumerate(message.string):
+                if not string:
+                    msgstrs.append(message.id[min(int(idx), 1)])
+                else:
+                    msgstrs.append(string)
+            msgstr = '\x00'.join([
+                msgstr.encode(catalog.charset) for msgstr in msgstrs
+            ])
+        else:
+            msgid = message.id.encode(catalog.charset)
+            if not message.string:
+                msgstr = message.id.encode(catalog.charset)
+            else:
+                msgstr = message.string.encode(catalog.charset)
+        if message.context:
+            msgid = '\x04'.join([message.context.encode(catalog.charset),
+                                 msgid])
+        offsets.append((len(ids), len(msgid), len(strs), len(msgstr)))
+        ids += msgid + '\x00'
+        strs += msgstr + '\x00'
+
+    # The header is 7 32-bit unsigned integers.  We don't use hash tables, so
+    # the keys start right after the index tables.
+    keystart = 7 * 4 + 16 * len(messages)
+    valuestart = keystart + len(ids)
+
+    # The string table first has the list of keys, then the list of values.
+    # Each entry has first the size of the string, then the file offset.
+    koffsets = []
+    voffsets = []
+    for o1, l1, o2, l2 in offsets:
+        koffsets += [l1, o1 + keystart]
+        voffsets += [l2, o2 + valuestart]
+    offsets = koffsets + voffsets
+
+    fileobj.write(struct.pack('Iiiiiii',
+        LE_MAGIC,                   # magic
+        0,                          # version
+        len(messages),              # number of entries
+        7 * 4,                      # start of key index
+        7 * 4 + len(messages) * 8,  # start of value index
+        0, 0                        # size and offset of hash table
+    ) + array.array("i", offsets).tostring() + ids + strs)
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/plurals.py
@@ -0,0 +1,255 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2008 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/.
+
+"""Plural form definitions."""
+
+from babel.core import default_locale, Locale
+from babel.util import itemgetter
+
+
+LC_CTYPE = default_locale('LC_CTYPE')
+
+
+PLURALS = {
+    # Afar
+    # 'aa': (),
+    # Abkhazian
+    # 'ab': (),
+    # Avestan
+    # 'ae': (),
+    # Afrikaans - From Pootle's PO's
+    'af': (2, '(n != 1)'),
+    # Akan
+    # 'ak': (),
+    # Amharic
+    # 'am': (),
+    # Aragonese
+    # 'an': (),
+    # Arabic - From Pootle's PO's
+    'ar': (6, '(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n>=3 && n<=10 ? 3 : n>=11 && n<=99 ? 4 : 5)'),
+    # Assamese
+    # 'as': (),
+    # Avaric
+    # 'av': (),
+    # Aymara
+    # 'ay': (),
+    # Azerbaijani
+    # 'az': (),
+    # Bashkir
+    # 'ba': (),
+    # Belarusian
+    # 'be': (),
+    # Bulgarian - From Pootle's PO's
+    'bg': (2, '(n != 1)'),
+    # Bihari
+    # 'bh': (),
+    # Bislama
+    # 'bi': (),
+    # Bambara
+    # 'bm': (),
+    # Bengali - From Pootle's PO's
+    'bn': (2, '(n != 1)'),
+    # Tibetan - as discussed in private with Andrew West
+    'bo': (1, '0'),
+    # Breton
+    # 'br': (),
+    # Bosnian
+    # 'bs': (),
+    # Catalan - From Pootle's PO's
+    'ca': (2, '(n != 1)'),
+    # Chechen
+    # 'ce': (),
+    # Chamorro
+    # 'ch': (),
+    # Corsican
+    # 'co': (),
+    # Cree
+    # 'cr': (),
+    # Czech
+    'cs': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Church Slavic
+    # 'cu': (),
+    # Chuvash
+    'cv': (1, '0'),
+    # Welsh
+    'cy': (5, '(n==1 ? 1 : n==2 ? 2 : n==3 ? 3 : n==6 ? 4 : 0)'),
+    # Danish
+    'da': (2, '(n != 1)'),
+    # German
+    'de': (2, '(n != 1)'),
+    # Divehi
+    # 'dv': (),
+    # Dzongkha
+    'dz': (1, '0'),
+    # Greek
+    'el': (2, '(n != 1)'),
+    # English
+    'en': (2, '(n != 1)'),
+    # Esperanto
+    'eo': (2, '(n != 1)'),
+    # Spanish
+    'es': (2, '(n != 1)'),
+    # Estonian
+    'et': (2, '(n != 1)'),
+    # Basque - From Pootle's PO's
+    'eu': (2, '(n != 1)'),
+    # Persian - From Pootle's PO's
+    'fa': (1, '0'),
+    # Finnish
+    'fi': (2, '(n != 1)'),
+    # French
+    'fr': (2, '(n > 1)'),
+    # Friulian - From Pootle's PO's
+    'fur': (2, '(n > 1)'),
+    # Irish
+    'ga': (3, '(n==1 ? 0 : n==2 ? 1 : 2)'),
+    # Galician - From Pootle's PO's
+    'gl': (2, '(n != 1)'),
+    # Hausa - From Pootle's PO's
+    'ha': (2, '(n != 1)'),
+    # Hebrew
+    'he': (2, '(n != 1)'),
+    # Hindi - From Pootle's PO's
+    'hi': (2, '(n != 1)'),
+    # Croatian
+    'hr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Hungarian
+    'hu': (1, '0'),
+    # Armenian - From Pootle's PO's
+    'hy': (1, '0'),
+    # Icelandic - From Pootle's PO's
+    'is': (2, '(n != 1)'),
+    # Italian
+    'it': (2, '(n != 1)'),
+    # Japanese
+    'ja': (1, '0'),
+    # Georgian - From Pootle's PO's
+    'ka': (1, '0'),
+    # Kongo - From Pootle's PO's
+    'kg': (2, '(n != 1)'),
+    # Khmer - From Pootle's PO's
+    'km': (1, '0'),
+    # Korean
+    'ko': (1, '0'),
+    # Kurdish - From Pootle's PO's
+    'ku': (2, '(n != 1)'),
+    # Lao - Another member of the Tai language family, like Thai.
+    'lo': (1, '0'),
+    # Lithuanian
+    'lt': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Latvian
+    'lv': (3, '(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2)'),
+    # Maltese - From Pootle's PO's
+    'mt': (4, '(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3)'),
+    # Norwegian Bokmål
+    'nb': (2, '(n != 1)'),
+    # Dutch
+    'nl': (2, '(n != 1)'),
+    # Norwegian Nynorsk
+    'nn': (2, '(n != 1)'),
+    # Norwegian
+    'no': (2, '(n != 1)'),
+    # Punjabi - From Pootle's PO's
+    'pa': (2, '(n != 1)'),
+    # Polish
+    'pl': (3, '(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Portuguese
+    'pt': (2, '(n != 1)'),
+    # Brazilian
+    'pt_BR': (2, '(n > 1)'),
+    # Romanian - From Pootle's PO's
+    'ro': (3, '(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2)'),
+    # Russian
+    'ru': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Slovak
+    'sk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Slovenian
+    'sl': (4, '(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3)'),
+    # Serbian - From Pootle's PO's
+    'sr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Southern Sotho - From Pootle's PO's
+    'st': (2, '(n != 1)'),
+    # Swedish
+    'sv': (2, '(n != 1)'),
+    # Thai
+    'th': (1, '0'),
+    # Turkish
+    'tr': (1, '0'),
+    # Ukrainian
+    'uk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    # Venda - From Pootle's PO's
+    've': (2, '(n != 1)'),
+    # Vietnamese - From Pootle's PO's
+    'vi': (1, '0'),
+    # Xhosa - From Pootle's PO's
+    'xh': (2, '(n != 1)'),
+    # Chinese - From Pootle's PO's
+    'zh_CN': (1, '0'),
+    'zh_HK': (1, '0'),
+    'zh_TW': (1, '0'),
+}
+
+
+DEFAULT_PLURAL = (2, '(n != 1)')
+
+
+class _PluralTuple(tuple):
+    """A tuple with plural information."""
+
+    __slots__ = ()
+    num_plurals = property(itemgetter(0), doc="""
+    The number of plurals used by the locale.""")
+    plural_expr = property(itemgetter(1), doc="""
+    The plural expression used by the locale.""")
+    plural_forms = property(lambda x: 'npurals=%s; plural=%s' % x, doc="""
+    The plural expression used by the catalog or locale.""")
+
+    def __str__(self):
+        return self.plural_forms
+
+
+def get_plural(locale=LC_CTYPE):
+    """A tuple with the information catalogs need to perform proper
+    pluralization.  The first item of the tuple is the number of plural
+    forms, the second the plural expression.
+
+    >>> get_plural(locale='en')
+    (2, '(n != 1)')
+    >>> get_plural(locale='ga')
+    (3, '(n==1 ? 0 : n==2 ? 1 : 2)')
+
+    The object returned is a special tuple with additional members:
+
+    >>> tup = get_plural("ja")
+    >>> tup.num_plurals
+    1
+    >>> tup.plural_expr
+    '0'
+    >>> tup.plural_forms
+    'npurals=1; plural=0'
+
+    Converting the tuple into a string prints the plural forms for a
+    gettext catalog:
+
+    >>> str(tup)
+    'npurals=1; plural=0'
+    """
+    locale = Locale.parse(locale)
+    try:
+        tup = PLURALS[str(locale)]
+    except KeyError:
+        try:
+            tup = PLURALS[locale.language]
+        except KeyError:
+            tup = DEFAULT_PLURAL
+    return _PluralTuple(tup)
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/pofile.py
@@ -0,0 +1,476 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2008 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>`_
+"""
+
+from datetime import date, datetime
+import os
+import re
+
+from babel import __version__ as VERSION
+from babel.messages.catalog import Catalog, Message
+from babel.util import set, wraptext, LOCALTZ
+
+__all__ = ['read_po', 'write_po']
+__docformat__ = 'restructuredtext en'
+
+def unescape(string):
+    r"""Reverse `escape` the given string.
+
+    >>> print unescape('"Say:\\n  \\"hello, world!\\"\\n"')
+    Say:
+      "hello, world!"
+    <BLANKLINE>
+
+    :param string: the string to unescape
+    :return: the unescaped string
+    :rtype: `str` or `unicode`
+    """
+    return string[1:-1].replace('\\\\', '\\') \
+                       .replace('\\t', '\t') \
+                       .replace('\\r', '\r') \
+                       .replace('\\n', '\n') \
+                       .replace('\\"', '\"')
+
+def denormalize(string):
+    r"""Reverse the normalization done by the `normalize` function.
+
+    >>> print denormalize(r'''""
+    ... "Say:\n"
+    ... "  \"hello, world!\"\n"''')
+    Say:
+      "hello, world!"
+    <BLANKLINE>
+
+    >>> print denormalize(r'''""
+    ... "Say:\n"
+    ... "  \"Lorem ipsum dolor sit "
+    ... "amet, consectetur adipisicing"
+    ... " elit, \"\n"''')
+    Say:
+      "Lorem ipsum dolor sit amet, consectetur adipisicing elit, "
+    <BLANKLINE>
+
+    :param string: the string to denormalize
+    :return: the denormalized string
+    :rtype: `unicode` or `str`
+    """
+    if string.startswith('""'):
+        lines = []
+        for line in string.splitlines()[1:]:
+            lines.append(unescape(line))
+        return ''.join(lines)
+    else:
+        return unescape(string)
+
+def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False):
+    """Read messages from a ``gettext`` PO (portable object) file from the given
+    file-like object and return a `Catalog`.
+
+    >>> from StringIO import StringIO
+    >>> buf = StringIO('''
+    ... #: main.py:1
+    ... #, fuzzy, python-format
+    ... msgid "foo %(name)s"
+    ... msgstr ""
+    ...
+    ... # A user comment
+    ... #. An auto comment
+    ... #: main.py:3
+    ... msgid "bar"
+    ... msgid_plural "baz"
+    ... msgstr[0] ""
+    ... msgstr[1] ""
+    ... ''')
+    >>> catalog = read_po(buf)
+    >>> catalog.revision_date = datetime(2007, 04, 01)
+
+    >>> for message in catalog:
+    ...     if message.id:
+    ...         print (message.id, message.string)
+    ...         print ' ', (message.locations, message.flags)
+    ...         print ' ', (message.user_comments, message.auto_comments)
+    (u'foo %(name)s', '')
+      ([(u'main.py', 1)], set([u'fuzzy', u'python-format']))
+      ([], [])
+    ((u'bar', u'baz'), ('', ''))
+      ([(u'main.py', 3)], set([]))
+      ([u'A user comment'], [u'An auto comment'])
+
+    :param fileobj: the file-like object to read the PO file from
+    :param locale: the locale identifier or `Locale` object, or `None`
+                   if the catalog is not bound to a locale (which basically
+                   means it's a template)
+    :param domain: the message domain
+    :param ignore_obsolete: whether to ignore obsolete messages in the input
+    :return: a catalog object representing the parsed PO file
+    :rtype: `Catalog`
+    """
+    catalog = Catalog(locale=locale, domain=domain)
+
+    counter = [0]
+    offset = [0]
+    messages = []
+    translations = []
+    locations = []
+    flags = []
+    user_comments = []
+    auto_comments = []
+    obsolete = [False]
+    context = []
+    in_msgid = [False]
+    in_msgstr = [False]
+    in_msgctxt = [False]
+
+    def _add_message():
+        translations.sort()
+        if len(messages) > 1:
+            msgid = tuple([denormalize(m) for m in messages])
+        else:
+            msgid = denormalize(messages[0])
+        if isinstance(msgid, (list, tuple)):
+            string = []
+            for idx in range(catalog.num_plurals):
+                try:
+                    string.append(translations[idx])
+                except IndexError:
+                    string.append((idx, ''))
+            string = tuple([denormalize(t[1]) for t in string])
+        else:
+            string = denormalize(translations[0][1])
+        if context:
+            msgctxt = denormalize('\n'.join(context))
+        else:
+            msgctxt = None
+        message = Message(msgid, string, list(locations), set(flags),
+                          auto_comments, user_comments, lineno=offset[0] + 1,
+                          context=msgctxt)
+        if obsolete[0]:
+            if not ignore_obsolete:
+                catalog.obsolete[msgid] = message
+        else:
+            catalog[msgid] = message
+        del messages[:]; del translations[:]; del context[:]; del locations[:];
+        del flags[:]; del auto_comments[:]; del user_comments[:];
+        obsolete[0] = False
+        counter[0] += 1
+
+    def _process_message_line(lineno, line):
+        if line.startswith('msgid_plural'):
+            in_msgid[0] = True
+            msg = line[12:].lstrip()
+            messages.append(msg)
+        elif line.startswith('msgid'):
+            in_msgid[0] = True
+            offset[0] = lineno
+            txt = line[5:].lstrip()
+            if messages:
+                _add_message()
+            messages.append(txt)
+        elif line.startswith('msgstr'):
+            in_msgid[0] = False
+            in_msgstr[0] = True
+            msg = line[6:].lstrip()
+            if msg.startswith('['):
+                idx, msg = msg[1:].split(']', 1)
+                translations.append([int(idx), msg.lstrip()])
+            else:
+                translations.append([0, msg])
+        elif line.startswith('msgctxt'):
+            if messages:
+                _add_message()
+            in_msgid[0] = in_msgstr[0] = False
+            context.append(line[7:].lstrip())
+        elif line.startswith('"'):
+            if in_msgid[0]:
+                messages[-1] += u'\n' + line.rstrip()
+            elif in_msgstr[0]:
+                translations[-1][1] += u'\n' + line.rstrip()
+            elif in_msgctxt[0]:
+                context.append(line.rstrip())
+
+    for lineno, line in enumerate(fileobj.readlines()):
+        line = line.strip()
+        if not isinstance(line, unicode):
+            line = line.decode(catalog.charset)
+        if line.startswith('#'):
+            in_msgid[0] = in_msgstr[0] = False
+            if messages and translations:
+                _add_message()
+            if line[1:].startswith(':'):
+                for location in line[2:].lstrip().split():
+                    pos = location.rfind(':')
+                    if pos >= 0:
+                        try:
+                            lineno = int(location[pos + 1:])
+                        except ValueError:
+                            continue
+                        locations.append((location[:pos], lineno))
+            elif line[1:].startswith(','):
+                for flag in line[2:].lstrip().split(','):
+                    flags.append(flag.strip())
+            elif line[1:].startswith('~'):
+                obsolete[0] = True
+                _process_message_line(lineno, line[2:].lstrip())
+            elif line[1:].startswith('.'):
+                # These are called auto-comments
+                comment = line[2:].strip()
+                if comment: # Just check that we're not adding empty comments
+                    auto_comments.append(comment)
+            else:
+                # These are called user comments
+                user_comments.append(line[1:].strip())
+        else:
+            _process_message_line(lineno, line)
+
+    if messages:
+        _add_message()
+
+    # No actual messages found, but there was some info in comments, from which
+    # we'll construct an empty header message
+    elif not counter[0] and (flags or user_comments or auto_comments):
+        messages.append(u'')
+        translations.append([0, u''])
+        _add_message()
+
+    return catalog
+
+WORD_SEP = re.compile('('
+    r'\s+|'                                 # any whitespace
+    r'[^\s\w]*\w+[a-zA-Z]-(?=\w+[a-zA-Z])|' # hyphenated words
+    r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w)'   # em-dash
+')')
+
+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 '"%s"' % string.replace('\\', '\\\\') \
+                          .replace('\t', '\\t') \
+                          .replace('\r', '\\r') \
+                          .replace('\n', '\\n') \
+                          .replace('\"', '\\"')
+
+def normalize(string, prefix='', width=76):
+    r"""Convert a string into a format that is appropriate for .po files.
+
+    >>> print normalize('''Say:
+    ...   "hello, world!"
+    ... ''', width=None)
+    ""
+    "Say:\n"
+    "  \"hello, world!\"\n"
+
+    >>> print normalize('''Say:
+    ...   "Lorem ipsum dolor sit amet, consectetur adipisicing elit, "
+    ... ''', width=32)
+    ""
+    "Say:\n"
+    "  \"Lorem ipsum dolor sit "
+    "amet, consectetur adipisicing"
+    " elit, \"\n"
+
+    :param string: the string to normalize
+    :param prefix: a string that should be prepended to every line
+    :param width: the maximum line width; use `None`, 0, or a negative number
+                  to completely disable line wrapping
+    :return: the normalized string
+    :rtype: `unicode`
+    """
+    if width and width > 0:
+        prefixlen = len(prefix)
+        lines = []
+        for idx, line in enumerate(string.splitlines(True)):
+            if len(escape(line)) + prefixlen > width:
+                chunks = WORD_SEP.split(line)
+                chunks.reverse()
+                while chunks:
+                    buf = []
+                    size = 2
+                    while chunks:
+                        l = len(escape(chunks[-1])) - 2 + prefixlen
+                        if size + l < width:
+                            buf.append(chunks.pop())
+                            size += l
+                        else:
+                            if not buf:
+                                # handle long chunks by putting them on a
+                                # separate line
+                                buf.append(chunks.pop())
+                            break
+                    lines.append(u''.join(buf))
+            else:
+                lines.append(line)
+    else:
+        lines = string.splitlines(True)
+
+    if len(lines) <= 1:
+        return escape(string)
+
+    # Remove empty trailing line
+    if lines and not lines[-1]:
+        del lines[-1]
+        lines[-1] += '\n'
+    return u'""\n' + u'\n'.join([(prefix + escape(l)) for l in lines])
+
+def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False,
+             sort_output=False, sort_by_file=False, ignore_obsolete=False,
+             include_previous=False):
+    r"""Write a ``gettext`` PO (portable object) template file for a given
+    message catalog to the provided file-like object.
+
+    >>> catalog = Catalog()
+    >>> catalog.add(u'foo %(name)s', locations=[('main.py', 1)],
+    ...             flags=('fuzzy',))
+    >>> catalog.add((u'bar', u'baz'), locations=[('main.py', 3)])
+    >>> from StringIO import StringIO
+    >>> buf = StringIO()
+    >>> write_po(buf, catalog, omit_header=True)
+    >>> print buf.getvalue()
+    #: main.py:1
+    #, fuzzy, python-format
+    msgid "foo %(name)s"
+    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 catalog: the `Catalog` instance
+    :param width: the maximum line width for the generated output; use `None`,
+                  0, or a negative number to completely disable line wrapping
+    :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
+    :param sort_output: whether to sort the messages in the output by msgid
+    :param sort_by_file: whether to sort the messages in the output by their
+                         locations
+    :param ignore_obsolete: whether to ignore obsolete messages and not include
+                            them in the output; by default they are included as
+                            comments
+    :param include_previous: include the old msgid as a comment when
+                             updating the catalog
+    """
+    def _normalize(key, prefix=''):
+        return normalize(key, prefix=prefix, width=width) \
+            .encode(catalog.charset, 'backslashreplace')
+
+    def _write(text):
+        if isinstance(text, unicode):
+            text = text.encode(catalog.charset)
+        fileobj.write(text)
+
+    def _write_comment(comment, prefix=''):
+        # xgettext always wraps comments even if --no-wrap is passed;
+        # provide the same behaviour
+        if width and width > 0:
+            _width = width
+        else:
+            _width = 76
+        for line in wraptext(comment, _width):
+            _write('#%s %s\n' % (prefix, line.strip()))
+
+    def _write_message(message, prefix=''):
+        if isinstance(message.id, (list, tuple)):
+            if message.context:
+                _write('%smsgctxt %s\n' % (prefix,
+                                           _normalize(message.context, prefix)))
+            _write('%smsgid %s\n' % (prefix, _normalize(message.id[0], prefix)))
+            _write('%smsgid_plural %s\n' % (
+                prefix, _normalize(message.id[1], prefix)
+            ))
+
+            for idx in range(catalog.num_plurals):
+                try:
+                    string = message.string[idx]
+                except IndexError:
+                    string = ''
+                _write('%smsgstr[%d] %s\n' % (
+                    prefix, idx, _normalize(string, prefix)
+                ))
+        else:
+            if message.context:
+                _write('%smsgctxt %s\n' % (prefix,
+                                           _normalize(message.context, prefix)))
+            _write('%smsgid %s\n' % (prefix, _normalize(message.id, prefix)))
+            _write('%smsgstr %s\n' % (
+                prefix, _normalize(message.string or '', prefix)
+            ))
+
+    messages = list(catalog)
+    if sort_output:
+        messages.sort()
+    elif sort_by_file:
+        messages.sort(lambda x,y: cmp(x.locations, y.locations))
+
+    for message in messages:
+        if not message.id: # This is the header "message"
+            if omit_header:
+                continue
+            comment_header = catalog.header_comment
+            if width and width > 0:
+                lines = []
+                for line in comment_header.splitlines():
+                    lines += wraptext(line, width=width,
+                                      subsequent_indent='# ')
+                comment_header = u'\n'.join(lines) + u'\n'
+            _write(comment_header)
+
+        for comment in message.user_comments:
+            _write_comment(comment)
+        for comment in message.auto_comments:
+            _write_comment(comment, prefix='.')
+
+        if not no_location:
+            locs = u' '.join([u'%s:%d' % (filename.replace(os.sep, '/'), lineno)
+                              for filename, lineno in message.locations])
+            _write_comment(locs, prefix=':')
+        if message.flags:
+            _write('#%s\n' % ', '.join([''] + list(message.flags)))
+
+        if message.previous_id and include_previous:
+            _write_comment('msgid %s' % _normalize(message.previous_id[0]),
+                           prefix='|')
+            if len(message.previous_id) > 1:
+                _write_comment('msgid_plural %s' % _normalize(
+                    message.previous_id[1]
+                ), prefix='|')
+
+        _write_message(message)
+        _write('\n')
+
+    if not ignore_obsolete:
+        for message in catalog.obsolete.values():
+            for comment in message.user_comments:
+                _write_comment(comment)
+            _write_message(message, prefix='#~ ')
+            _write('\n')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/__init__.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2008 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.messages.tests import catalog, extract, frontend, mofile, \
+                                     plurals, pofile, checkers
+    suite = unittest.TestSuite()
+    suite.addTest(catalog.suite())
+    suite.addTest(extract.suite())
+    suite.addTest(frontend.suite())
+    suite.addTest(mofile.suite())
+    suite.addTest(plurals.suite())
+    suite.addTest(pofile.suite())
+    suite.addTest(checkers.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/catalog.py
@@ -0,0 +1,293 @@
+# -*- 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
+import datetime
+import doctest
+import unittest
+
+from babel.messages import catalog
+
+
+class MessageTestCase(unittest.TestCase):
+
+    def test_python_format(self):
+        assert catalog.PYTHON_FORMAT.search('foo %d bar')
+        assert catalog.PYTHON_FORMAT.search('foo %s bar')
+        assert catalog.PYTHON_FORMAT.search('foo %r bar')
+        assert catalog.PYTHON_FORMAT.search('foo %(name).1f')
+        assert catalog.PYTHON_FORMAT.search('foo %(name)3.3f')
+        assert catalog.PYTHON_FORMAT.search('foo %(name)3f')
+        assert catalog.PYTHON_FORMAT.search('foo %(name)06d')
+        assert catalog.PYTHON_FORMAT.search('foo %(name)Li')
+        assert catalog.PYTHON_FORMAT.search('foo %(name)#d')
+        assert catalog.PYTHON_FORMAT.search('foo %(name)-4.4hs')
+        assert catalog.PYTHON_FORMAT.search('foo %(name)*.3f')
+        assert catalog.PYTHON_FORMAT.search('foo %(name).*f')
+        assert catalog.PYTHON_FORMAT.search('foo %(name)3.*f')
+        assert catalog.PYTHON_FORMAT.search('foo %(name)*.*f')
+        assert catalog.PYTHON_FORMAT.search('foo %()s')
+
+    def test_translator_comments(self):
+        mess = catalog.Message('foo', user_comments=['Comment About `foo`'])
+        self.assertEqual(mess.user_comments, ['Comment About `foo`'])
+        mess = catalog.Message('foo',
+                               auto_comments=['Comment 1 About `foo`',
+                                         'Comment 2 About `foo`'])
+        self.assertEqual(mess.auto_comments, ['Comment 1 About `foo`',
+                                         'Comment 2 About `foo`'])
+
+    def test_clone_message_object(self):
+        msg = catalog.Message('foo', locations=[('foo.py', 42)])
+        clone = msg.clone()
+        clone.locations.append(('bar.py', 42))
+        self.assertEqual(msg.locations, [('foo.py', 42)])
+        msg.flags.add('fuzzy')
+        assert not clone.fuzzy and msg.fuzzy
+
+
+class CatalogTestCase(unittest.TestCase):
+
+    def test_two_messages_with_same_singular(self):
+        cat = catalog.Catalog()
+        cat.add('foo')
+        cat.add(('foo', 'foos'))
+        self.assertEqual(1, len(cat))
+
+    def test_duplicate_auto_comment(self):
+        cat = catalog.Catalog()
+        cat.add('foo', auto_comments=['A comment'])
+        cat.add('foo', auto_comments=['A comment', 'Another comment'])
+        self.assertEqual(['A comment', 'Another comment'],
+                         cat['foo'].auto_comments)
+
+    def test_duplicate_user_comment(self):
+        cat = catalog.Catalog()
+        cat.add('foo', user_comments=['A comment'])
+        cat.add('foo', user_comments=['A comment', 'Another comment'])
+        self.assertEqual(['A comment', 'Another comment'],
+                         cat['foo'].user_comments)
+
+    def test_duplicate_location(self):
+        cat = catalog.Catalog()
+        cat.add('foo', locations=[('foo.py', 1)])
+        cat.add('foo', locations=[('foo.py', 1)])
+        self.assertEqual([('foo.py', 1)], cat['foo'].locations)
+
+    def test_update_message_changed_to_plural(self):
+        cat = catalog.Catalog()
+        cat.add(u'foo', u'Voh')
+        tmpl = catalog.Catalog()
+        tmpl.add((u'foo', u'foos'))
+        cat.update(tmpl)
+        self.assertEqual((u'Voh', ''), cat['foo'].string)
+        assert cat['foo'].fuzzy
+
+    def test_update_message_changed_to_simple(self):
+        cat = catalog.Catalog()
+        cat.add((u'foo' u'foos'), (u'Voh', u'Vöhs'))
+        tmpl = catalog.Catalog()
+        tmpl.add(u'foo')
+        cat.update(tmpl)
+        self.assertEqual(u'Voh', cat['foo'].string)
+        assert cat['foo'].fuzzy
+
+    def test_update_message_updates_comments(self):
+        cat = catalog.Catalog()
+        cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 5)])
+        self.assertEqual(cat[u'foo'].auto_comments, [])
+        self.assertEqual(cat[u'foo'].user_comments, [])
+        # Update cat[u'foo'] with a new location and a comment
+        cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 7)],
+                                      user_comments=['Foo Bar comment 1'])
+        self.assertEqual(cat[u'foo'].user_comments, ['Foo Bar comment 1'])
+        # now add yet another location with another comment
+        cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 9)],
+                                      auto_comments=['Foo Bar comment 2'])
+        self.assertEqual(cat[u'foo'].auto_comments, ['Foo Bar comment 2'])
+
+    def test_update_fuzzy_matching_with_case_change(self):
+        cat = catalog.Catalog()
+        cat.add('foo', 'Voh')
+        cat.add('bar', 'Bahr')
+        tmpl = catalog.Catalog()
+        tmpl.add('Foo')
+        cat.update(tmpl)
+        self.assertEqual(1, len(cat.obsolete))
+        assert 'foo' not in cat
+
+        self.assertEqual('Voh', cat['Foo'].string)
+        self.assertEqual(True, cat['Foo'].fuzzy)
+
+    def test_update_fuzzy_matching_with_char_change(self):
+        cat = catalog.Catalog()
+        cat.add('fo', 'Voh')
+        cat.add('bar', 'Bahr')
+        tmpl = catalog.Catalog()
+        tmpl.add('foo')
+        cat.update(tmpl)
+        self.assertEqual(1, len(cat.obsolete))
+        assert 'fo' not in cat
+
+        self.assertEqual('Voh', cat['foo'].string)
+        self.assertEqual(True, cat['foo'].fuzzy)
+
+    def test_update_fuzzy_matching_no_msgstr(self):
+        cat = catalog.Catalog()
+        cat.add('fo', '')
+        tmpl = catalog.Catalog()
+        tmpl.add('fo')
+        tmpl.add('foo')
+        cat.update(tmpl)
+        assert 'fo' in cat
+        assert 'foo' in cat
+
+        self.assertEqual('', cat['fo'].string)
+        self.assertEqual(False, cat['fo'].fuzzy)
+        self.assertEqual(None, cat['foo'].string)
+        self.assertEqual(False, cat['foo'].fuzzy)
+
+    def test_update_fuzzy_matching_with_new_context(self):
+        cat = catalog.Catalog()
+        cat.add('foo', 'Voh')
+        cat.add('bar', 'Bahr')
+        tmpl = catalog.Catalog()
+        tmpl.add('Foo', context='Menu')
+        cat.update(tmpl)
+        self.assertEqual(1, len(cat.obsolete))
+        assert 'foo' not in cat
+
+        message = cat.get('Foo', 'Menu')
+        self.assertEqual('Voh', message.string)
+        self.assertEqual(True, message.fuzzy)
+        self.assertEqual('Menu', message.context)
+
+    def test_update_fuzzy_matching_with_changed_context(self):
+        cat = catalog.Catalog()
+        cat.add('foo', 'Voh', context='Menu|File')
+        cat.add('bar', 'Bahr', context='Menu|File')
+        tmpl = catalog.Catalog()
+        tmpl.add('Foo', context='Menu|Edit')
+        cat.update(tmpl)
+        self.assertEqual(1, len(cat.obsolete))
+        assert cat.get('Foo', 'Menu|File') is None
+
+        message = cat.get('Foo', 'Menu|Edit')
+        self.assertEqual('Voh', message.string)
+        self.assertEqual(True, message.fuzzy)
+        self.assertEqual('Menu|Edit', message.context)
+
+    def test_update_fuzzy_matching_no_cascading(self):
+        cat = catalog.Catalog()
+        cat.add('fo', 'Voh')
+        cat.add('foo', 'Vohe')
+        tmpl = catalog.Catalog()
+        tmpl.add('fo')
+        tmpl.add('foo')
+        tmpl.add('fooo')
+        cat.update(tmpl)
+        assert 'fo' in cat
+        assert 'foo' in cat
+
+        self.assertEqual('Voh', cat['fo'].string)
+        self.assertEqual(False, cat['fo'].fuzzy)
+        self.assertEqual('Vohe', cat['foo'].string)
+        self.assertEqual(False, cat['foo'].fuzzy)
+        self.assertEqual('Vohe', cat['fooo'].string)
+        self.assertEqual(True, cat['fooo'].fuzzy)
+
+    def test_update_without_fuzzy_matching(self):
+        cat = catalog.Catalog()
+        cat.add('fo', 'Voh')
+        cat.add('bar', 'Bahr')
+        tmpl = catalog.Catalog()
+        tmpl.add('foo')
+        cat.update(tmpl, no_fuzzy_matching=True)
+        self.assertEqual(2, len(cat.obsolete))
+
+    def test_fuzzy_matching_regarding_plurals(self):
+        cat = catalog.Catalog()
+        cat.add(('foo', 'foh'), ('foo', 'foh'))
+        ru = copy.copy(cat)
+        ru.locale = 'ru_RU'
+        ru.update(cat)
+        self.assertEqual(True, ru['foo'].fuzzy)
+        ru = copy.copy(cat)
+        ru.locale = 'ru_RU'
+        ru['foo'].string = ('foh', 'fohh', 'fohhh')
+        ru.update(cat)
+        self.assertEqual(False, ru['foo'].fuzzy)
+
+    def test_update_no_template_mutation(self):
+        tmpl = catalog.Catalog()
+        tmpl.add('foo')
+        cat1 = catalog.Catalog()
+        cat1.add('foo', 'Voh')
+        cat1.update(tmpl)
+        cat2 = catalog.Catalog()
+        cat2.update(tmpl)
+
+        self.assertEqual(None, cat2['foo'].string)
+        self.assertEqual(False, cat2['foo'].fuzzy)
+        
+    def test_update_po_updates_pot_creation_date(self):
+        template = catalog.Catalog()
+        localized_catalog = copy.deepcopy(template)
+        localized_catalog.locale = 'de_DE'
+        self.assertNotEqual(template.mime_headers,
+                            localized_catalog.mime_headers)
+        self.assertEqual(template.creation_date,
+                         localized_catalog.creation_date)
+        template.creation_date = datetime.datetime.now() - \
+                                                datetime.timedelta(minutes=5)
+        localized_catalog.update(template)
+        self.assertEqual(template.creation_date,
+                         localized_catalog.creation_date)
+        
+    def test_update_po_keeps_po_revision_date(self):
+        template = catalog.Catalog()
+        localized_catalog = copy.deepcopy(template)
+        localized_catalog.locale = 'de_DE'
+        fake_rev_date = datetime.datetime.now() - datetime.timedelta(days=5)
+        localized_catalog.revision_date = fake_rev_date
+        self.assertNotEqual(template.mime_headers,
+                            localized_catalog.mime_headers)
+        self.assertEqual(template.creation_date,
+                         localized_catalog.creation_date)
+        template.creation_date = datetime.datetime.now() - \
+                                                datetime.timedelta(minutes=5)
+        localized_catalog.update(template)
+        self.assertEqual(localized_catalog.revision_date, fake_rev_date)
+
+    def test_stores_datetime_correctly(self):
+        localized = catalog.Catalog()
+        localized.locale = 'de_DE'
+        localized[''] = catalog.Message('', 
+                       "POT-Creation-Date: 2009-03-09 15:47-0700\n" +
+                       "PO-Revision-Date: 2009-03-09 15:47-0700\n")
+        for key, value in localized.mime_headers:
+            if key in ('POT-Creation-Date', 'PO-Revision-Date'):
+                self.assertEqual(value, '2009-03-09 15:47-0700')
+
+def suite():
+    suite = unittest.TestSuite()
+    if hasattr(doctest, 'ELLIPSIS'):
+        suite.addTest(doctest.DocTestSuite(catalog, optionflags=doctest.ELLIPSIS))
+    else:
+        suite.addTest(doctest.DocTestSuite(catalog))
+    suite.addTest(unittest.makeSuite(MessageTestCase))
+    suite.addTest(unittest.makeSuite(CatalogTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/checkers.py
@@ -0,0 +1,371 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008 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/.
+
+from datetime import datetime
+import time
+import unittest
+from StringIO import StringIO
+
+from babel import __version__ as VERSION
+from babel.core import Locale, UnknownLocaleError
+from babel.dates import format_datetime
+from babel.messages import checkers
+from babel.messages.plurals import PLURALS
+from babel.messages.pofile import read_po
+from babel.util import LOCALTZ
+
+
+class CheckersTestCase(unittest.TestCase):
+    # the last msgstr[idx] is always missing except for singular plural forms
+
+    def test_1_num_plurals_checkers(self):
+        for _locale in [p for p in PLURALS if PLURALS[p][0] == 1]:
+            try:
+                locale = Locale.parse(_locale)
+            except UnknownLocaleError:
+                # Just an alias? Not what we're testing here, let's continue
+                continue
+            po_file = (ur"""\
+# %(english_name)s translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: %(locale)s <LL@li.org>\n"
+"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator comment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+
+""" % dict(locale       = _locale,
+           english_name = locale.english_name,
+           version      = VERSION,
+           year         = time.strftime('%Y'),
+           date         = format_datetime(datetime.now(LOCALTZ),
+                                          'yyyy-MM-dd HH:mmZ',
+                                          tzinfo=LOCALTZ, locale=_locale),
+           num_plurals  = PLURALS[_locale][0],
+           plural_expr  = PLURALS[_locale][0])).encode('utf-8')
+
+            # This test will fail for revisions <= 406 because so far
+            # catalog.num_plurals was neglected
+            catalog = read_po(StringIO(po_file), _locale)
+            message = catalog['foobar']
+            checkers.num_plurals(catalog, message)
+
+    def test_2_num_plurals_checkers(self):
+        # in this testcase we add an extra msgstr[idx], we should be
+        # disregarding it
+        for _locale in [p for p in PLURALS if PLURALS[p][0] == 2]:
+            if _locale in ['nn', 'no']:
+                _locale = 'nn_NO'
+                num_plurals  = PLURALS[_locale.split('_')[0]][0]
+                plural_expr  = PLURALS[_locale.split('_')[0]][1]
+            else:
+                num_plurals  = PLURALS[_locale][0]
+                plural_expr  = PLURALS[_locale][1]
+            try:
+                locale = Locale(_locale)
+                date = format_datetime(datetime.now(LOCALTZ),
+                                       'yyyy-MM-dd HH:mmZ',
+                                       tzinfo=LOCALTZ, locale=_locale)
+            except UnknownLocaleError:
+                # Just an alias? Not what we're testing here, let's continue
+                continue
+            po_file = (ur"""\
+# %(english_name)s translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: %(locale)s <LL@li.org>\n"
+"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator comment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+""" % dict(locale       = _locale,
+           english_name = locale.english_name,
+           version      = VERSION,
+           year         = time.strftime('%Y'),
+           date         = date,
+           num_plurals  = num_plurals,
+           plural_expr  = plural_expr)).encode('utf-8')
+            # we should be adding the missing msgstr[0]
+
+            # This test will fail for revisions <= 406 because so far
+            # catalog.num_plurals was neglected
+            catalog = read_po(StringIO(po_file), _locale)
+            message = catalog['foobar']
+            checkers.num_plurals(catalog, message)
+
+    def test_3_num_plurals_checkers(self):
+        for _locale in [p for p in PLURALS if PLURALS[p][0] == 3]:
+            po_file = r"""\
+# %(english_name)s translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: %(locale)s <LL@li.org>\n"
+"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator comment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+""" % dict(locale       = _locale,
+           english_name = Locale.parse(_locale).english_name,
+           version      = VERSION,
+           year         = time.strftime('%Y'),
+           date         = format_datetime(datetime.now(LOCALTZ),
+                                          'yyyy-MM-dd HH:mmZ',
+                                          tzinfo=LOCALTZ, locale=_locale),
+           num_plurals  = PLURALS[_locale][0],
+           plural_expr  = PLURALS[_locale][0])
+
+            # This test will fail for revisions <= 406 because so far
+            # catalog.num_plurals was neglected
+            catalog = read_po(StringIO(po_file), _locale)
+            message = catalog['foobar']
+            checkers.num_plurals(catalog, message)
+
+    def test_4_num_plurals_checkers(self):
+        for _locale in [p for p in PLURALS if PLURALS[p][0] == 4]:
+            po_file = r"""\
+# %(english_name)s translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: %(locale)s <LL@li.org>\n"
+"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator comment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+""" % dict(locale       = _locale,
+           english_name = Locale.parse(_locale).english_name,
+           version      = VERSION,
+           year         = time.strftime('%Y'),
+           date         = format_datetime(datetime.now(LOCALTZ),
+                                          'yyyy-MM-dd HH:mmZ',
+                                          tzinfo=LOCALTZ, locale=_locale),
+           num_plurals  = PLURALS[_locale][0],
+           plural_expr  = PLURALS[_locale][0])
+
+            # This test will fail for revisions <= 406 because so far
+            # catalog.num_plurals was neglected
+            catalog = read_po(StringIO(po_file), _locale)
+            message = catalog['foobar']
+            checkers.num_plurals(catalog, message)
+
+    def test_5_num_plurals_checkers(self):
+        for _locale in [p for p in PLURALS if PLURALS[p][0] == 5]:
+            po_file = r"""\
+# %(english_name)s translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: %(locale)s <LL@li.org>\n"
+"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator comment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+""" % dict(locale       = _locale,
+           english_name = Locale.parse(_locale).english_name,
+           version      = VERSION,
+           year         = time.strftime('%Y'),
+           date         = format_datetime(datetime.now(LOCALTZ),
+                                          'yyyy-MM-dd HH:mmZ',
+                                          tzinfo=LOCALTZ, locale=_locale),
+           num_plurals  = PLURALS[_locale][0],
+           plural_expr  = PLURALS[_locale][0])
+
+            # This test will fail for revisions <= 406 because so far
+            # catalog.num_plurals was neglected
+            catalog = read_po(StringIO(po_file), _locale)
+            message = catalog['foobar']
+            checkers.num_plurals(catalog, message)
+
+    def test_6_num_plurals_checkers(self):
+        for _locale in [p for p in PLURALS if PLURALS[p][0] == 6]:
+            po_file = r"""\
+# %(english_name)s translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: %(locale)s <LL@li.org>\n"
+"Plural-Forms: nplurals=%(num_plurals)s; plural=%(plural_expr)s\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator comment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+msgstr[4] ""
+
+""" % dict(locale       = _locale,
+           english_name = Locale.parse(_locale).english_name,
+           version      = VERSION,
+           year         = time.strftime('%Y'),
+           date         = format_datetime(datetime.now(LOCALTZ),
+                                          'yyyy-MM-dd HH:mmZ',
+                                          tzinfo=LOCALTZ, locale=_locale),
+           num_plurals  = PLURALS[_locale][0],
+           plural_expr  = PLURALS[_locale][0])
+
+            # This test will fail for revisions <= 406 because so far
+            # catalog.num_plurals was neglected
+            catalog = read_po(StringIO(po_file), _locale)
+            message = catalog['foobar']
+            checkers.num_plurals(catalog, message)
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(CheckersTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/data/mapping.cfg
@@ -0,0 +1,5 @@
+# Ignore directory
+[ignore: **/ignored/**.*]
+
+# Extraction from Python source files
+[python: **.py]
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/data/project/file1.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+# file1.py for tests
+
+from gettext import gettext as _
+def foo():
+    # TRANSLATOR: This will be a translator coment,
+    # that will include several lines
+    print _('bar')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/data/project/file2.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# file2.py for tests
+
+from gettext import ngettext
+
+def foo():
+    # Note: This will have the TRANSLATOR: tag but shouldn't
+    # be included on the extracted stuff
+    print ngettext('foobar', 'foobars', 1)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..21b1727014d7301c78245ccf10b82e484b40c36b
GIT binary patch
literal 547
zc${63&u-H|5XQIVPbD8XAdVb{8>kwyc2hNMNtLv5P$V0o#Bha8JaLVj-Dr1R(bquY
z^>`L0q*A1g^wZ4Fe7l<c{=R(rM{qo0UNb%B8Iy~B{CJpY<^@B{?;{}s=jY7Hu@J9z
zGtNii5W73ayJD%GSX+BYUd){RNvnX<3cr!_jWr5N^5LNmGESm7ZLJGf_`0d^x~n~w
zR>6{sSXY%J?}xO}O&f={ie_I*ym6!hKZRo*NI@#)1<D@E1d?-gCQl`IScr4FYy4x~
z1)fPrQ<e1hx?KB!rPIc@I#{P*@-@%l^Qg$+V^)l&`EWEI&$GqiilwIRbWK>2-YBT(
z`<O*}K5U!Vy80@bwVl%~PORH_1!nvI=m+T~?90Jh18)Y9yo-uyksTaKVj1076Ugu?
z_gmgI(68`*+v>(#!s?ctq%i2h8v6&`Zsj#O%*@JGjj0v%mraO1ks-(bWh?I$T<ayZ
a?DB_-?$D*Z^GGZLe}lxNyY-JMBmMz*ew2m)
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/data/project/i18n/de/LC_MESSAGES/messages.po
@@ -0,0 +1,32 @@
+# German (Germany) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: 2007-07-30 22:18+0200\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: de_DE <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9dev-r245\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr "Stange"
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] "Fuhstange"
+msgstr[1] "Fuhstangen"
+
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/data/project/i18n/de_DE/LC_MESSAGES/messages.po
@@ -0,0 +1,33 @@
+# German (Germany) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: 2007-07-30 22:18+0200\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: de_DE <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9dev-r245\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/data/project/i18n/messages.pot
@@ -0,0 +1,32 @@
+# Translations template for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.1\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/data/project/i18n/messages_non_fuzzy.pot
@@ -0,0 +1,31 @@
+# Translations template for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.1\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/data/project/i18n/ru_RU/LC_MESSAGES/messages.po
@@ -0,0 +1,34 @@
+# Russian (Russia) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: 2007-07-30 22:18+0200\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: ru_RU <LL@li.org>\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9dev-r363\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/data/project/ignored/a_test_file.txt
@@ -0,0 +1,1 @@
+Just a test file.
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/data/project/ignored/this_wont_normally_be_here.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+
+# This file won't normally be in this directory.
+# It IS only for tests
+
+from gettext import ngettext
+
+def foo():
+    # Note: This will have the TRANSLATOR: tag but shouldn't
+    # be included on the extracted stuff
+    print ngettext('FooBar', 'FooBars', 1)
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/data/setup.cfg
@@ -0,0 +1,5 @@
+[extract_messages]
+msgid_bugs_address = bugs.address@email.tld
+copyright_holder = FooBar, TM
+add_comments = TRANSLATOR:,TRANSLATORS:
+output_file = project/i18n/project.pot
new file mode 100755
--- /dev/null
+++ b/babel3/babel/messages/tests/data/setup.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vim: sw=4 ts=4 fenc=utf-8
+# =============================================================================
+# $Id$
+# =============================================================================
+# $URL$
+# $LastChangedDate$
+# $Rev$
+# $LastChangedBy$
+# =============================================================================
+# Copyright (C) 2006 Ufsoft.org - Pedro Algarvio <ufs@ufsoft.org>
+#
+# Please view LICENSE for additional licensing information.
+# =============================================================================
+
+# THIS IS A BOGUS PROJECT
+
+from setuptools import setup, find_packages
+
+setup(
+    name = 'TestProject',
+    version = '0.1',
+    license = 'BSD',
+    author = 'Foo Bar',
+    author_email = 'foo@bar.tld',
+    packages = find_packages(),
+)
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/extract.py
@@ -0,0 +1,533 @@
+# -*- 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 codecs
+import doctest
+from StringIO import StringIO
+import sys
+import unittest
+
+from babel.messages import extract
+
+
+class ExtractPythonTestCase(unittest.TestCase):
+
+    def test_nested_calls(self):
+        buf = StringIO("""\
+msg1 = _(i18n_arg.replace(r'\"', '"'))
+msg2 = ungettext(i18n_arg.replace(r'\"', '"'), multi_arg.replace(r'\"', '"'), 2)
+msg3 = ungettext("Babel", multi_arg.replace(r'\"', '"'), 2)
+msg4 = ungettext(i18n_arg.replace(r'\"', '"'), "Babels", 2)
+msg5 = ungettext('bunny', 'bunnies', random.randint(1, 2))
+msg6 = ungettext(arg0, 'bunnies', random.randint(1, 2))
+msg7 = _(hello.there)
+msg8 = gettext('Rabbit')
+msg9 = dgettext('wiki', model.addPage())
+msg10 = dngettext(getDomain(), 'Page', 'Pages', 3)
+""")
+        messages = list(extract.extract_python(buf,
+                                               extract.DEFAULT_KEYWORDS.keys(),
+                                               [], {}))
+        self.assertEqual([
+                (1, '_', None, []),
+                (2, 'ungettext', (None, None, None), []),
+                (3, 'ungettext', (u'Babel', None, None), []),
+                (4, 'ungettext', (None, u'Babels', None), []),
+                (5, 'ungettext', (u'bunny', u'bunnies', None), []),
+                (6, 'ungettext', (None, u'bunnies', None), []),
+                (7, '_', None, []),
+                (8, 'gettext', u'Rabbit', []),
+                (9, 'dgettext', (u'wiki', None), []),
+                (10, 'dngettext', (None, u'Page', u'Pages', None), [])],
+                         messages)
+
+    def test_nested_comments(self):
+        buf = StringIO("""\
+msg = ngettext('pylon',  # TRANSLATORS: shouldn't be
+               'pylons', # TRANSLATORS: seeing this
+               count)
+""")
+        messages = list(extract.extract_python(buf, ('ngettext',),
+                                               ['TRANSLATORS:'], {}))
+        self.assertEqual([(1, 'ngettext', (u'pylon', u'pylons', None), [])],
+                         messages)
+
+    def test_comments_with_calls_that_spawn_multiple_lines(self):
+        buf = StringIO("""\
+# NOTE: This Comment SHOULD Be Extracted
+add_notice(req, ngettext("Catalog deleted.",
+                         "Catalogs deleted.", len(selected)))
+
+# NOTE: This Comment SHOULD Be Extracted
+add_notice(req, _("Locale deleted."))
+
+
+# NOTE: This Comment SHOULD Be Extracted
+add_notice(req, ngettext("Foo deleted.", "Foos deleted.", len(selected)))
+
+# NOTE: This Comment SHOULD Be Extracted
+# NOTE: And This One Too
+add_notice(req, ngettext("Bar deleted.",
+                         "Bars deleted.", len(selected)))
+""")
+        messages = list(extract.extract_python(buf, ('ngettext','_'), ['NOTE:'],
+
+                                               {'strip_comment_tags':False}))
+        self.assertEqual((6, '_', 'Locale deleted.',
+                          [u'NOTE: This Comment SHOULD Be Extracted']),
+                         messages[1])
+        self.assertEqual((10, 'ngettext', (u'Foo deleted.', u'Foos deleted.',
+                                           None),
+                          [u'NOTE: This Comment SHOULD Be Extracted']),
+                         messages[2])
+        self.assertEqual((3, 'ngettext',
+                           (u'Catalog deleted.',
+                            u'Catalogs deleted.', None),
+                           [u'NOTE: This Comment SHOULD Be Extracted']),
+                         messages[0])
+        self.assertEqual((15, 'ngettext', (u'Bar deleted.', u'Bars deleted.',
+                                           None),
+                          [u'NOTE: This Comment SHOULD Be Extracted',
+                           u'NOTE: And This One Too']),
+                         messages[3])
+
+    def test_declarations(self):
+        buf = StringIO("""\
+class gettext(object):
+    pass
+def render_body(context,x,y=_('Page arg 1'),z=_('Page arg 2'),**pageargs):
+    pass
+def ngettext(y='arg 1',z='arg 2',**pageargs):
+    pass
+class Meta:
+    verbose_name = _('log entry')
+""")
+        messages = list(extract.extract_python(buf,
+                                               extract.DEFAULT_KEYWORDS.keys(),
+                                               [], {}))
+        self.assertEqual([(3, '_', u'Page arg 1', []),
+                          (3, '_', u'Page arg 2', []),
+                          (8, '_', u'log entry', [])],
+                         messages)
+
+    def test_multiline(self):
+        buf = StringIO("""\
+msg1 = ngettext('pylon',
+                'pylons', count)
+msg2 = ngettext('elvis',
+                'elvises',
+                 count)
+""")
+        messages = list(extract.extract_python(buf, ('ngettext',), [], {}))
+        self.assertEqual([(1, 'ngettext', (u'pylon', u'pylons', None), []),
+                          (3, 'ngettext', (u'elvis', u'elvises', None), [])],
+                         messages)
+
+    def test_triple_quoted_strings(self):
+        buf = StringIO("""\
+msg1 = _('''pylons''')
+msg2 = ngettext(r'''elvis''', \"\"\"elvises\"\"\", count)
+msg2 = ngettext(\"\"\"elvis\"\"\", 'elvises', count)
+""")
+        messages = list(extract.extract_python(buf,
+                                               extract.DEFAULT_KEYWORDS.keys(),
+                                               [], {}))
+        self.assertEqual([(1, '_', (u'pylons'), []),
+                          (2, 'ngettext', (u'elvis', u'elvises', None), []),
+                          (3, 'ngettext', (u'elvis', u'elvises', None), [])],
+                         messages)
+
+    def test_multiline_strings(self):
+        buf = StringIO("""\
+_('''This module provides internationalization and localization
+support for your Python programs by providing an interface to the GNU
+gettext message catalog library.''')
+""")
+        messages = list(extract.extract_python(buf,
+                                               extract.DEFAULT_KEYWORDS.keys(),
+                                               [], {}))
+        self.assertEqual(
+            [(1, '_',
+              u'This module provides internationalization and localization\n'
+              'support for your Python programs by providing an interface to '
+              'the GNU\ngettext message catalog library.', [])],
+            messages)
+
+    def test_concatenated_strings(self):
+        buf = StringIO("""\
+foobar = _('foo' 'bar')
+""")
+        messages = list(extract.extract_python(buf,
+                                               extract.DEFAULT_KEYWORDS.keys(),
+                                               [], {}))
+        self.assertEqual(u'foobar', messages[0][2])
+
+    def test_unicode_string_arg(self):
+        buf = StringIO("msg = _(u'Foo Bar')")
+        messages = list(extract.extract_python(buf, ('_',), [], {}))
+        self.assertEqual(u'Foo Bar', messages[0][2])
+
+    def test_comment_tag(self):
+        buf = StringIO("""
+# NOTE: A translation comment
+msg = _(u'Foo Bar')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Foo Bar', messages[0][2])
+        self.assertEqual([u'NOTE: A translation comment'], messages[0][3])
+
+    def test_comment_tag_multiline(self):
+        buf = StringIO("""
+# NOTE: A translation comment
+# with a second line
+msg = _(u'Foo Bar')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Foo Bar', messages[0][2])
+        self.assertEqual([u'NOTE: A translation comment', u'with a second line'],
+                         messages[0][3])
+
+    def test_translator_comments_with_previous_non_translator_comments(self):
+        buf = StringIO("""
+# This shouldn't be in the output
+# because it didn't start with a comment tag
+# NOTE: A translation comment
+# with a second line
+msg = _(u'Foo Bar')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Foo Bar', messages[0][2])
+        self.assertEqual([u'NOTE: A translation comment', u'with a second line'],
+                         messages[0][3])
+
+    def test_comment_tags_not_on_start_of_comment(self):
+        buf = StringIO("""
+# This shouldn't be in the output
+# because it didn't start with a comment tag
+# do NOTE: this will not be a translation comment
+# NOTE: This one will be
+msg = _(u'Foo Bar')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Foo Bar', messages[0][2])
+        self.assertEqual([u'NOTE: This one will be'], messages[0][3])
+
+    def test_multiple_comment_tags(self):
+        buf = StringIO("""
+# NOTE1: A translation comment for tag1
+# with a second line
+msg = _(u'Foo Bar1')
+
+# NOTE2: A translation comment for tag2
+msg = _(u'Foo Bar2')
+""")
+        messages = list(extract.extract_python(buf, ('_',),
+                                               ['NOTE1:', 'NOTE2:'], {}))
+        self.assertEqual(u'Foo Bar1', messages[0][2])
+        self.assertEqual([u'NOTE1: A translation comment for tag1',
+                          u'with a second line'], messages[0][3])
+        self.assertEqual(u'Foo Bar2', messages[1][2])
+        self.assertEqual([u'NOTE2: A translation comment for tag2'], messages[1][3])
+
+    def test_two_succeeding_comments(self):
+        buf = StringIO("""
+# NOTE: one
+# NOTE: two
+msg = _(u'Foo Bar')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Foo Bar', messages[0][2])
+        self.assertEqual([u'NOTE: one', u'NOTE: two'], messages[0][3])
+
+    def test_invalid_translator_comments(self):
+        buf = StringIO("""
+# NOTE: this shouldn't apply to any messages
+hello = 'there'
+
+msg = _(u'Foo Bar')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Foo Bar', messages[0][2])
+        self.assertEqual([], messages[0][3])
+
+    def test_invalid_translator_comments2(self):
+        buf = StringIO("""
+# NOTE: Hi!
+hithere = _('Hi there!')
+
+# NOTE: you should not be seeing this in the .po
+rows = [[v for v in range(0,10)] for row in range(0,10)]
+
+# this (NOTE:) should not show up either
+hello = _('Hello')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Hi there!', messages[0][2])
+        self.assertEqual([u'NOTE: Hi!'], messages[0][3])
+        self.assertEqual(u'Hello', messages[1][2])
+        self.assertEqual([], messages[1][3])
+
+    def test_invalid_translator_comments3(self):
+        buf = StringIO("""
+# NOTE: Hi,
+
+# there!
+hithere = _('Hi there!')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Hi there!', messages[0][2])
+        self.assertEqual([], messages[0][3])
+
+    def test_comment_tag_with_leading_space(self):
+        buf = StringIO("""
+  #: A translation comment
+  #: with leading spaces
+msg = _(u'Foo Bar')
+""")
+        messages = list(extract.extract_python(buf, ('_',), [':'], {}))
+        self.assertEqual(u'Foo Bar', messages[0][2])
+        self.assertEqual([u': A translation comment', u': with leading spaces'],
+                         messages[0][3])
+
+    def test_different_signatures(self):
+        buf = StringIO("""
+foo = _('foo', 'bar')
+n = ngettext('hello', 'there', n=3)
+n = ngettext(n=3, 'hello', 'there')
+n = ngettext(n=3, *messages)
+n = ngettext()
+n = ngettext('foo')
+""")
+        messages = list(extract.extract_python(buf, ('_', 'ngettext'), [], {}))
+        self.assertEqual((u'foo', u'bar'), messages[0][2])
+        self.assertEqual((u'hello', u'there', None), messages[1][2])
+        self.assertEqual((None, u'hello', u'there'), messages[2][2])
+        self.assertEqual((None, None), messages[3][2])
+        self.assertEqual(None, messages[4][2])
+        self.assertEqual(('foo'), messages[5][2])
+
+    def test_utf8_message(self):
+        buf = StringIO("""
+# NOTE: hello
+msg = _('Bonjour à tous')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'],
+                                               {'encoding': 'utf-8'}))
+        self.assertEqual(u'Bonjour à tous', messages[0][2])
+        self.assertEqual([u'NOTE: hello'], messages[0][3])
+
+    def test_utf8_message_with_magic_comment(self):
+        buf = StringIO("""# -*- coding: utf-8 -*-
+# NOTE: hello
+msg = _('Bonjour à tous')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Bonjour à tous', messages[0][2])
+        self.assertEqual([u'NOTE: hello'], messages[0][3])
+
+    def test_utf8_message_with_utf8_bom(self):
+        buf = StringIO(codecs.BOM_UTF8 + """
+# NOTE: hello
+msg = _('Bonjour à tous')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Bonjour à tous', messages[0][2])
+        self.assertEqual([u'NOTE: hello'], messages[0][3])
+
+    def test_utf8_raw_strings_match_unicode_strings(self):
+        buf = StringIO(codecs.BOM_UTF8 + """
+msg = _('Bonjour à tous')
+msgu = _(u'Bonjour à tous')
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Bonjour à tous', messages[0][2])
+        self.assertEqual(messages[0][2], messages[1][2])
+
+    def test_extract_strip_comment_tags(self):
+        buf = StringIO("""\
+#: This is a comment with a very simple
+#: prefix specified
+_('Servus')
+
+# NOTE: This is a multiline comment with
+# a prefix too
+_('Babatschi')""")
+        messages = list(extract.extract('python', buf, comment_tags=['NOTE:', ':'],
+                                        strip_comment_tags=True))
+        self.assertEqual(u'Servus', messages[0][1])
+        self.assertEqual([u'This is a comment with a very simple',
+                          u'prefix specified'], messages[0][2])
+        self.assertEqual(u'Babatschi', messages[1][1])
+        self.assertEqual([u'This is a multiline comment with',
+                          u'a prefix too'], messages[1][2])
+
+
+class ExtractJavaScriptTestCase(unittest.TestCase):
+
+    def test_simple_extract(self):
+        buf = StringIO("""\
+msg1 = _('simple')
+msg2 = gettext('simple')
+msg3 = ngettext('s', 'p', 42)
+        """)
+        messages = \
+            list(extract.extract('javascript', buf, extract.DEFAULT_KEYWORDS,
+                                 [], {}))
+
+        self.assertEqual([(1, 'simple', []),
+                          (2, 'simple', []),
+                          (3, ('s', 'p'), [])], messages)
+
+    def test_various_calls(self):
+        buf = StringIO("""\
+msg1 = _(i18n_arg.replace(/"/, '"'))
+msg2 = ungettext(i18n_arg.replace(/"/, '"'), multi_arg.replace(/"/, '"'), 2)
+msg3 = ungettext("Babel", multi_arg.replace(/"/, '"'), 2)
+msg4 = ungettext(i18n_arg.replace(/"/, '"'), "Babels", 2)
+msg5 = ungettext('bunny', 'bunnies', parseInt(Math.random() * 2 + 1))
+msg6 = ungettext(arg0, 'bunnies', rparseInt(Math.random() * 2 + 1))
+msg7 = _(hello.there)
+msg8 = gettext('Rabbit')
+msg9 = dgettext('wiki', model.addPage())
+msg10 = dngettext(domain, 'Page', 'Pages', 3)
+""")
+        messages = \
+            list(extract.extract('javascript', buf, extract.DEFAULT_KEYWORDS, [],
+                                 {}))
+        self.assertEqual([(5, (u'bunny', u'bunnies'), []),
+                          (8, u'Rabbit', []),
+                          (10, (u'Page', u'Pages'), [])], messages)
+
+    def test_message_with_line_comment(self):
+        buf = StringIO("""\
+// NOTE: hello
+msg = _('Bonjour à tous')
+""")
+        messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Bonjour à tous', messages[0][2])
+        self.assertEqual([u'NOTE: hello'], messages[0][3])
+
+    def test_message_with_multiline_comment(self):
+        buf = StringIO("""\
+/* NOTE: hello
+   and bonjour
+     and servus */
+msg = _('Bonjour à tous')
+""")
+        messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Bonjour à tous', messages[0][2])
+        self.assertEqual([u'NOTE: hello', 'and bonjour', '  and servus'], messages[0][3])
+
+    def test_ignore_function_definitions(self):
+        buf = StringIO("""\
+function gettext(value) {
+    return translations[language][value] || value;
+}""")
+
+        messages = list(extract.extract_javascript(buf, ('gettext',), [], {}))
+        self.assertEqual(messages, [])
+
+    def test_misplaced_comments(self):
+        buf = StringIO("""\
+/* NOTE: this won't show up */
+foo()
+
+/* NOTE: this will */
+msg = _('Something')
+
+// NOTE: this will show up
+// too.
+msg = _('Something else')
+
+// NOTE: but this won't
+bar()
+
+_('no comment here')
+""")
+        messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual(u'Something', messages[0][2])
+        self.assertEqual([u'NOTE: this will'], messages[0][3])
+        self.assertEqual(u'Something else', messages[1][2])
+        self.assertEqual([u'NOTE: this will show up', 'too.'], messages[1][3])
+        self.assertEqual(u'no comment here', messages[2][2])
+        self.assertEqual([], messages[2][3])
+
+
+class ExtractTestCase(unittest.TestCase):
+
+    def test_invalid_filter(self):
+        buf = StringIO("""\
+msg1 = _(i18n_arg.replace(r'\"', '"'))
+msg2 = ungettext(i18n_arg.replace(r'\"', '"'), multi_arg.replace(r'\"', '"'), 2)
+msg3 = ungettext("Babel", multi_arg.replace(r'\"', '"'), 2)
+msg4 = ungettext(i18n_arg.replace(r'\"', '"'), "Babels", 2)
+msg5 = ungettext('bunny', 'bunnies', random.randint(1, 2))
+msg6 = ungettext(arg0, 'bunnies', random.randint(1, 2))
+msg7 = _(hello.there)
+msg8 = gettext('Rabbit')
+msg9 = dgettext('wiki', model.addPage())
+msg10 = dngettext(domain, 'Page', 'Pages', 3)
+""")
+        messages = \
+            list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [],
+                                 {}))
+        self.assertEqual([(5, (u'bunny', u'bunnies'), []),
+                          (8, u'Rabbit', []),
+                          (10, (u'Page', u'Pages'), [])], messages)
+
+    def test_invalid_extract_method(self):
+        buf = StringIO('')
+        self.assertRaises(ValueError, list, extract.extract('spam', buf))
+
+    def test_different_signatures(self):
+        buf = StringIO("""
+foo = _('foo', 'bar')
+n = ngettext('hello', 'there', n=3)
+n = ngettext(n=3, 'hello', 'there')
+n = ngettext(n=3, *messages)
+n = ngettext()
+n = ngettext('foo')
+""")
+        messages = \
+            list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [],
+                                 {}))
+        self.assertEqual(len(messages), 2)
+        self.assertEqual(u'foo', messages[0][1])
+        self.assertEqual((u'hello', u'there'), messages[1][1])
+
+    def test_empty_string_msgid(self):
+        buf = StringIO("""\
+msg = _('')
+""")
+        stderr = sys.stderr
+        sys.stderr = StringIO()
+        try:
+            messages = \
+                list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS,
+                                     [], {}))
+            self.assertEqual([], messages)
+            assert 'warning: Empty msgid.' in sys.stderr.getvalue()
+        finally:
+            sys.stderr = stderr
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(extract))
+    suite.addTest(unittest.makeSuite(ExtractPythonTestCase))
+    suite.addTest(unittest.makeSuite(ExtractJavaScriptTestCase))
+    suite.addTest(unittest.makeSuite(ExtractTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/frontend.py
@@ -0,0 +1,874 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2008 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/.
+
+from datetime import datetime
+from distutils.dist import Distribution
+from distutils.errors import DistutilsOptionError
+from distutils.log import _global_log
+import doctest
+import os
+import shutil
+from StringIO import StringIO
+import sys
+import time
+import unittest
+
+from babel import __version__ as VERSION
+from babel.dates import format_datetime
+from babel.messages import frontend
+from babel.util import LOCALTZ
+
+
+class CompileCatalogTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.olddir = os.getcwd()
+        self.datadir = os.path.join(os.path.dirname(__file__), 'data')
+        os.chdir(self.datadir)
+        _global_log.threshold = 5 # shut up distutils logging
+
+        self.dist = Distribution(dict(
+            name='TestProject',
+            version='0.1',
+            packages=['project']
+        ))
+        self.cmd = frontend.compile_catalog(self.dist)
+        self.cmd.initialize_options()
+
+    def tearDown(self):
+        os.chdir(self.olddir)
+
+    def test_no_directory_or_output_file_specified(self):
+        self.cmd.locale = 'en_US'
+        self.cmd.input_file = 'dummy'
+        self.assertRaises(DistutilsOptionError, self.cmd.finalize_options)
+
+    def test_no_directory_or_input_file_specified(self):
+        self.cmd.locale = 'en_US'
+        self.cmd.output_file = 'dummy'
+        self.assertRaises(DistutilsOptionError, self.cmd.finalize_options)
+
+
+class ExtractMessagesTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.olddir = os.getcwd()
+        self.datadir = os.path.join(os.path.dirname(__file__), 'data')
+        os.chdir(self.datadir)
+        _global_log.threshold = 5 # shut up distutils logging
+
+        self.dist = Distribution(dict(
+            name='TestProject',
+            version='0.1',
+            packages=['project']
+        ))
+        self.cmd = frontend.extract_messages(self.dist)
+        self.cmd.initialize_options()
+
+    def tearDown(self):
+        pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot')
+        if os.path.isfile(pot_file):
+            os.unlink(pot_file)
+
+        os.chdir(self.olddir)
+
+    def test_neither_default_nor_custom_keywords(self):
+        self.cmd.output_file = 'dummy'
+        self.cmd.no_default_keywords = True
+        self.assertRaises(DistutilsOptionError, self.cmd.finalize_options)
+
+    def test_no_output_file_specified(self):
+        self.assertRaises(DistutilsOptionError, self.cmd.finalize_options)
+
+    def test_both_sort_output_and_sort_by_file(self):
+        self.cmd.output_file = 'dummy'
+        self.cmd.sort_output = True
+        self.cmd.sort_by_file = True
+        self.assertRaises(DistutilsOptionError, self.cmd.finalize_options)
+
+    def test_extraction_with_default_mapping(self):
+        self.cmd.copyright_holder = 'FooBar, Inc.'
+        self.cmd.msgid_bugs_address = 'bugs.address@email.tld'
+        self.cmd.output_file = 'project/i18n/temp.pot'
+        self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:'
+
+        self.cmd.finalize_options()
+        self.cmd.run()
+
+        pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot')
+        assert os.path.isfile(pot_file)
+
+        self.assertEqual(
+r"""# Translations template for TestProject.
+# Copyright (C) %(year)s FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, %(year)s.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: %(date)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=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. TRANSLATOR: This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+#: project/ignored/this_wont_normally_be_here.py:11
+msgid "FooBar"
+msgid_plural "FooBars"
+msgstr[0] ""
+msgstr[1] ""
+
+""" % {'version': VERSION,
+       'year': time.strftime('%Y'),
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='en')},
+        open(pot_file, 'U').read())
+
+    def test_extraction_with_mapping_file(self):
+        self.cmd.copyright_holder = 'FooBar, Inc.'
+        self.cmd.msgid_bugs_address = 'bugs.address@email.tld'
+        self.cmd.mapping_file = 'mapping.cfg'
+        self.cmd.output_file = 'project/i18n/temp.pot'
+        self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:'
+
+        self.cmd.finalize_options()
+        self.cmd.run()
+
+        pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot')
+        assert os.path.isfile(pot_file)
+
+        self.assertEqual(
+r"""# Translations template for TestProject.
+# Copyright (C) %(year)s FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, %(year)s.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: %(date)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=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. TRANSLATOR: This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+""" % {'version': VERSION,
+       'year': time.strftime('%Y'),
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='en')},
+        open(pot_file, 'U').read())
+
+    def test_extraction_with_mapping_dict(self):
+        self.dist.message_extractors = {
+            'project': [
+                ('**/ignored/**.*', 'ignore',   None),
+                ('**.py',           'python',   None),
+            ]
+        }
+        self.cmd.copyright_holder = 'FooBar, Inc.'
+        self.cmd.msgid_bugs_address = 'bugs.address@email.tld'
+        self.cmd.output_file = 'project/i18n/temp.pot'
+        self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:'
+
+        self.cmd.finalize_options()
+        self.cmd.run()
+
+        pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot')
+        assert os.path.isfile(pot_file)
+
+        self.assertEqual(
+r"""# Translations template for TestProject.
+# Copyright (C) %(year)s FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, %(year)s.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: %(date)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=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. TRANSLATOR: This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+""" % {'version': VERSION,
+       'year': time.strftime('%Y'),
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='en')},
+        open(pot_file, 'U').read())
+
+
+class InitCatalogTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.olddir = os.getcwd()
+        self.datadir = os.path.join(os.path.dirname(__file__), 'data')
+        os.chdir(self.datadir)
+        _global_log.threshold = 5 # shut up distutils logging
+
+        self.dist = Distribution(dict(
+            name='TestProject',
+            version='0.1',
+            packages=['project']
+        ))
+        self.cmd = frontend.init_catalog(self.dist)
+        self.cmd.initialize_options()
+
+    def tearDown(self):
+        for dirname in ['en_US', 'ja_JP', 'lv_LV']:
+            locale_dir = os.path.join(self.datadir, 'project', 'i18n', dirname)
+            if os.path.isdir(locale_dir):
+                shutil.rmtree(locale_dir)
+
+        os.chdir(self.olddir)
+
+    def test_no_input_file(self):
+        self.cmd.locale = 'en_US'
+        self.cmd.output_file = 'dummy'
+        self.assertRaises(DistutilsOptionError, self.cmd.finalize_options)
+
+    def test_no_locale(self):
+        self.cmd.input_file = 'dummy'
+        self.cmd.output_file = 'dummy'
+        self.assertRaises(DistutilsOptionError, self.cmd.finalize_options)
+
+    def test_with_output_dir(self):
+        self.cmd.input_file = 'project/i18n/messages.pot'
+        self.cmd.locale = 'en_US'
+        self.cmd.output_dir = 'project/i18n'
+
+        self.cmd.finalize_options()
+        self.cmd.run()
+
+        po_file = os.path.join(self.datadir, 'project', 'i18n', 'en_US',
+                               'LC_MESSAGES', 'messages.po')
+        assert os.path.isfile(po_file)
+
+        self.assertEqual(
+r"""# English (United States) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: en_US <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+""" % {'version': VERSION,
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='en')},
+       open(po_file, 'U').read())
+
+    def test_keeps_catalog_non_fuzzy(self):
+        self.cmd.input_file = 'project/i18n/messages_non_fuzzy.pot'
+        self.cmd.locale = 'en_US'
+        self.cmd.output_dir = 'project/i18n'
+
+        self.cmd.finalize_options()
+        self.cmd.run()
+
+        po_file = os.path.join(self.datadir, 'project', 'i18n', 'en_US',
+                               'LC_MESSAGES', 'messages.po')
+        assert os.path.isfile(po_file)
+
+        self.assertEqual(
+r"""# English (United States) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: en_US <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+""" % {'version': VERSION,
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='en')},
+       open(po_file, 'U').read())
+
+    def test_correct_init_more_than_2_plurals(self):
+        self.cmd.input_file = 'project/i18n/messages.pot'
+        self.cmd.locale = 'lv_LV'
+        self.cmd.output_dir = 'project/i18n'
+
+        self.cmd.finalize_options()
+        self.cmd.run()
+
+        po_file = os.path.join(self.datadir, 'project', 'i18n', 'lv_LV',
+                               'LC_MESSAGES', 'messages.po')
+        assert os.path.isfile(po_file)
+
+        self.assertEqual(
+r"""# Latvian (Latvia) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: lv_LV <LL@li.org>\n"
+"Plural-Forms: nplurals=3; plural=(n%%10==1 && n%%100!=11 ? 0 : n != 0 ? 1 :"
+" 2)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+""" % {'version': VERSION,
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='en')},
+       open(po_file, 'U').read())
+
+    def test_correct_init_singular_plural_forms(self):
+        self.cmd.input_file = 'project/i18n/messages.pot'
+        self.cmd.locale = 'ja_JP'
+        self.cmd.output_dir = 'project/i18n'
+
+        self.cmd.finalize_options()
+        self.cmd.run()
+
+        po_file = os.path.join(self.datadir, 'project', 'i18n', 'ja_JP',
+                               'LC_MESSAGES', 'messages.po')
+        assert os.path.isfile(po_file)
+
+        self.assertEqual(
+r"""# Japanese (Japan) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: ja_JP <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+
+""" % {'version': VERSION,
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='ja_JP')},
+       open(po_file, 'U').read())
+
+
+class CommandLineInterfaceTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.datadir = os.path.join(os.path.dirname(__file__), 'data')
+        self.orig_argv = sys.argv
+        self.orig_stdout = sys.stdout
+        self.orig_stderr = sys.stderr
+        sys.argv = ['pybabel']
+        sys.stdout = StringIO()
+        sys.stderr = StringIO()
+        self.cli = frontend.CommandLineInterface()
+
+    def tearDown(self):
+        sys.argv = self.orig_argv
+        sys.stdout = self.orig_stdout
+        sys.stderr = self.orig_stderr
+        for dirname in ['lv_LV', 'ja_JP']: 
+            locale_dir = os.path.join(self.datadir, 'project', 'i18n', dirname)
+            if os.path.isdir(locale_dir):
+                shutil.rmtree(locale_dir)
+
+    def test_usage(self):
+        try:
+            self.cli.run(sys.argv)
+            self.fail('Expected SystemExit')
+        except SystemExit, e:
+            self.assertEqual(2, e.code)
+            self.assertEqual("""\
+usage: pybabel command [options] [args]
+
+pybabel: error: no valid command or option passed. try the -h/--help option for more information.
+""", sys.stderr.getvalue().lower())
+
+    def test_help(self):
+        try:
+            self.cli.run(sys.argv + ['--help'])
+            self.fail('Expected SystemExit')
+        except SystemExit, e:
+            self.assertEqual(0, e.code)
+            self.assertEqual("""\
+usage: pybabel command [options] [args]
+
+options:
+  --version       show program's version number and exit
+  -h, --help      show this help message and exit
+  --list-locales  print all known locales and exit
+  -v, --verbose   print as much as possible
+  -q, --quiet     print as little as possible
+
+commands:
+  compile  compile message catalogs to mo files
+  extract  extract messages from source files and generate a pot file
+  init     create new message catalogs from a pot file
+  update   update existing message catalogs from a pot file
+""", sys.stdout.getvalue().lower())
+
+    def test_extract_with_default_mapping(self):
+        pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot')
+        try:
+            self.cli.run(sys.argv + ['extract',
+                '--copyright-holder', 'FooBar, Inc.',
+                '--msgid-bugs-address', 'bugs.address@email.tld',
+                '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:',
+                '-o', pot_file, os.path.join(self.datadir, 'project')])
+        except SystemExit, e:
+            self.assertEqual(0, e.code)
+            assert os.path.isfile(pot_file)
+            self.assertEqual(
+r"""# Translations template for TestProject.
+# Copyright (C) %(year)s FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, %(year)s.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: %(date)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=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+#: project/ignored/this_wont_normally_be_here.py:11
+msgid "FooBar"
+msgid_plural "FooBars"
+msgstr[0] ""
+msgstr[1] ""
+
+""" % {'version': VERSION,
+       'year': time.strftime('%Y'),
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='en')},
+       open(pot_file, 'U').read())
+
+    def test_extract_with_mapping_file(self):
+        pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot')
+        try:
+            self.cli.run(sys.argv + ['extract',
+                '--copyright-holder', 'FooBar, Inc.',
+                '--msgid-bugs-address', 'bugs.address@email.tld',
+                '--mapping', os.path.join(self.datadir, 'mapping.cfg'),
+                '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:',
+                '-o', pot_file, os.path.join(self.datadir, 'project')])
+        except SystemExit, e:
+            self.assertEqual(0, e.code)
+            assert os.path.isfile(pot_file)
+            self.assertEqual(
+r"""# Translations template for TestProject.
+# Copyright (C) %(year)s FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, %(year)s.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: %(date)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=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+""" % {'version': VERSION,
+       'year': time.strftime('%Y'),
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='en')},
+       open(pot_file, 'U').read())
+
+    def test_init_with_output_dir(self):
+        po_file = os.path.join(self.datadir, 'project', 'i18n', 'en_US',
+                               'LC_MESSAGES', 'messages.po')
+        try:
+            self.cli.run(sys.argv + ['init',
+                '--locale', 'en_US',
+                '-d', os.path.join(self.datadir, 'project', 'i18n'),
+                '-i', os.path.join(self.datadir, 'project', 'i18n',
+                                   'messages.pot')])
+        except SystemExit, e:
+            self.assertEqual(0, e.code)
+            assert os.path.isfile(po_file)
+            self.assertEqual(
+r"""# English (United States) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: en_US <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+""" % {'version': VERSION,
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='en')},
+       open(po_file, 'U').read())
+            
+    def test_init_singular_plural_forms(self):
+        po_file = os.path.join(self.datadir, 'project', 'i18n', 'ja_JP',
+                               'LC_MESSAGES', 'messages.po')
+        try:
+            self.cli.run(sys.argv + ['init',
+                '--locale', 'ja_JP',
+                '-d', os.path.join(self.datadir, 'project', 'i18n'),
+                '-i', os.path.join(self.datadir, 'project', 'i18n',
+                                   'messages.pot')])
+        except SystemExit, e:
+            self.assertEqual(0, e.code)
+        assert os.path.isfile(po_file)
+        self.assertEqual(
+r"""# Japanese (Japan) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: ja_JP <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+
+""" % {'version': VERSION,
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='en')},
+       open(po_file, 'U').read())
+            
+    def test_init_more_than_2_plural_forms(self):
+        po_file = os.path.join(self.datadir, 'project', 'i18n', 'lv_LV',
+                               'LC_MESSAGES', 'messages.po')
+        try:
+            self.cli.run(sys.argv + ['init',
+                '--locale', 'lv_LV',
+                '-d', os.path.join(self.datadir, 'project', 'i18n'),
+                '-i', os.path.join(self.datadir, 'project', 'i18n',
+                                   'messages.pot')])
+        except SystemExit, e:
+            self.assertEqual(0, e.code)
+        assert os.path.isfile(po_file)
+        self.assertEqual(
+r"""# Latvian (Latvia) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: %(date)s\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: lv_LV <LL@li.org>\n"
+"Plural-Forms: nplurals=3; plural=(n%%10==1 && n%%100!=11 ? 0 : n != 0 ? 1 :"
+" 2)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel %(version)s\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+""" % {'version': VERSION,
+       'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
+                               tzinfo=LOCALTZ, locale='en')},
+       open(po_file, 'U').read())
+
+    def test_compile_catalog(self):
+        po_file = os.path.join(self.datadir, 'project', 'i18n', 'de_DE',
+                               'LC_MESSAGES', 'messages.po')
+        mo_file = po_file.replace('.po', '.mo')
+        self.cli.run(sys.argv + ['compile',
+            '--locale', 'de_DE',
+            '-d', os.path.join(self.datadir, 'project', 'i18n')])
+        assert not os.path.isfile(mo_file), 'Expected no file at %r' % mo_file
+        self.assertEqual("""\
+catalog %r is marked as fuzzy, skipping
+""" % (po_file), sys.stderr.getvalue())
+
+    def test_compile_fuzzy_catalog(self):
+        po_file = os.path.join(self.datadir, 'project', 'i18n', 'de_DE',
+                               'LC_MESSAGES', 'messages.po')
+        mo_file = po_file.replace('.po', '.mo')
+        try:
+            self.cli.run(sys.argv + ['compile',
+                '--locale', 'de_DE', '--use-fuzzy',
+                '-d', os.path.join(self.datadir, 'project', 'i18n')])
+            assert os.path.isfile(mo_file)
+            self.assertEqual("""\
+compiling catalog %r to %r
+""" % (po_file, mo_file), sys.stderr.getvalue())
+        finally:
+            if os.path.isfile(mo_file):
+                os.unlink(mo_file)
+
+    def test_compile_catalog_with_more_than_2_plural_forms(self):
+        po_file = os.path.join(self.datadir, 'project', 'i18n', 'ru_RU',
+                               'LC_MESSAGES', 'messages.po')
+        mo_file = po_file.replace('.po', '.mo')
+        try:
+            self.cli.run(sys.argv + ['compile',
+                '--locale', 'ru_RU', '--use-fuzzy',
+                '-d', os.path.join(self.datadir, 'project', 'i18n')])
+            assert os.path.isfile(mo_file)
+            self.assertEqual("""\
+compiling catalog %r to %r
+""" % (po_file, mo_file), sys.stderr.getvalue())
+        finally:
+            if os.path.isfile(mo_file):
+                os.unlink(mo_file)
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(frontend))
+    suite.addTest(unittest.makeSuite(CompileCatalogTestCase))
+    suite.addTest(unittest.makeSuite(ExtractMessagesTestCase))
+    suite.addTest(unittest.makeSuite(InitCatalogTestCase))
+    suite.addTest(unittest.makeSuite(CommandLineInterfaceTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/mofile.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2008 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 gettext
+import os
+import unittest
+from StringIO import StringIO
+
+from babel.messages import mofile, Catalog
+
+
+class ReadMoTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.datadir = os.path.join(os.path.dirname(__file__), 'data')
+
+    def test_basics(self):
+        mo_file = open(os.path.join(self.datadir, 'project', 'i18n', 'de',
+                                    'LC_MESSAGES', 'messages.mo'))
+        try:
+            catalog = mofile.read_mo(mo_file)
+            self.assertEqual(2, len(catalog))
+            self.assertEqual('TestProject', catalog.project)
+            self.assertEqual('0.1', catalog.version)
+            self.assertEqual('Stange', catalog['bar'].string)
+            self.assertEqual(['Fuhstange', 'Fuhstangen'],
+                             catalog['foobar'].string)
+        finally:
+            mo_file.close()
+
+
+class WriteMoTestCase(unittest.TestCase):
+
+    def test_sorting(self):
+        # Ensure the header is sorted to the first entry so that its charset
+        # can be applied to all subsequent messages by GNUTranslations
+        # (ensuring all messages are safely converted to unicode)
+        catalog = Catalog(locale='en_US')
+        catalog.add(u'', '''\
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n''')
+        catalog.add(u'foo', 'Voh')
+        catalog.add((u'There is', u'There are'), (u'Es gibt', u'Es gibt'))
+        catalog.add(u'Fizz', '')
+        catalog.add(('Fuzz', 'Fuzzes'), ('', ''))
+        buf = StringIO()
+        mofile.write_mo(buf, catalog)
+        buf.seek(0)
+        translations = gettext.GNUTranslations(fp=buf)
+        self.assertEqual(u'Voh', translations.ugettext('foo'))
+        assert isinstance(translations.ugettext('foo'), unicode)
+        self.assertEqual(u'Es gibt', translations.ungettext('There is', 'There are', 1))
+        assert isinstance(translations.ungettext('There is', 'There are', 1), unicode)
+        self.assertEqual(u'Fizz', translations.ugettext('Fizz'))
+        assert isinstance(translations.ugettext('Fizz'), unicode)
+        self.assertEqual(u'Fuzz', translations.ugettext('Fuzz'))
+        assert isinstance(translations.ugettext('Fuzz'), unicode)
+        self.assertEqual(u'Fuzzes', translations.ugettext('Fuzzes'))
+        assert isinstance(translations.ugettext('Fuzzes'), unicode)
+
+    def test_more_plural_forms(self):
+        catalog2 = Catalog(locale='ru_RU')
+        catalog2.add(('Fuzz', 'Fuzzes'), ('', '', ''))
+        buf = StringIO()
+        mofile.write_mo(buf, catalog2)
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(mofile))
+    suite.addTest(unittest.makeSuite(ReadMoTestCase))
+    suite.addTest(unittest.makeSuite(WriteMoTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/plurals.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008 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.messages import plurals
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(plurals))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/messages/tests/pofile.py
@@ -0,0 +1,507 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2010 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/.
+
+from datetime import datetime
+import doctest
+from StringIO import StringIO
+import unittest
+
+from babel.messages.catalog import Catalog, Message
+from babel.messages import pofile
+from babel.util import FixedOffsetTimezone, LOCALTZ
+
+
+class ReadPoTestCase(unittest.TestCase):
+
+    def test_preserve_locale(self):
+        buf = StringIO(r'''msgid "foo"
+msgstr "Voh"''')
+        catalog = pofile.read_po(buf, locale='en_US')
+        self.assertEqual('en_US', catalog.locale)
+
+    def test_preserve_domain(self):
+        buf = StringIO(r'''msgid "foo"
+msgstr "Voh"''')
+        catalog = pofile.read_po(buf, domain='mydomain')
+        self.assertEqual('mydomain', catalog.domain)
+
+    def test_read_multiline(self):
+        buf = StringIO(r'''msgid ""
+"Here's some text that\n"
+"includesareallylongwordthatmightbutshouldnt"
+" throw us into an infinite "
+"loop\n"
+msgstr ""''')
+        catalog = pofile.read_po(buf)
+        self.assertEqual(1, len(catalog))
+        message = list(catalog)[1]
+        self.assertEqual("Here's some text that\nincludesareallylongwordthat"
+                         "mightbutshouldnt throw us into an infinite loop\n",
+                         message.id)
+
+    def test_fuzzy_header(self):
+        buf = StringIO(r'''\
+# Translations template for AReallyReallyLongNameForAProject.
+# Copyright (C) 2007 ORGANIZATION
+# This file is distributed under the same license as the
+# AReallyReallyLongNameForAProject project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy
+''')
+        catalog = pofile.read_po(buf)
+        self.assertEqual(1, len(list(catalog)))
+        self.assertEqual(True, list(catalog)[0].fuzzy)
+
+    def test_not_fuzzy_header(self):
+        buf = StringIO(r'''\
+# Translations template for AReallyReallyLongNameForAProject.
+# Copyright (C) 2007 ORGANIZATION
+# This file is distributed under the same license as the
+# AReallyReallyLongNameForAProject project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+''')
+        catalog = pofile.read_po(buf)
+        self.assertEqual(1, len(list(catalog)))
+        self.assertEqual(False, list(catalog)[0].fuzzy)
+
+    def test_header_entry(self):
+        buf = StringIO(r'''\
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2007 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version:  3.15\n"
+"Report-Msgid-Bugs-To: Fliegender Zirkus <fliegender@zirkus.de>\n"
+"POT-Creation-Date: 2007-09-27 11:19+0700\n"
+"PO-Revision-Date: 2007-09-27 21:42-0700\n"
+"Last-Translator: John <cleese@bavaria.de>\n"
+"Language-Team: German Lang <de@babel.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=iso-8859-2\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 1.0dev-r313\n"
+''')
+        catalog = pofile.read_po(buf)
+        self.assertEqual(1, len(list(catalog)))
+        self.assertEqual(u'3.15', catalog.version)
+        self.assertEqual(u'Fliegender Zirkus <fliegender@zirkus.de>',
+                         catalog.msgid_bugs_address)
+        self.assertEqual(datetime(2007, 9, 27, 11, 19,
+                                  tzinfo=FixedOffsetTimezone(7 * 60)),
+                         catalog.creation_date)
+        self.assertEqual(u'John <cleese@bavaria.de>', catalog.last_translator)
+        self.assertEqual(u'German Lang <de@babel.org>', catalog.language_team)
+        self.assertEqual(u'iso-8859-2', catalog.charset)
+        self.assertEqual(True, list(catalog)[0].fuzzy)
+
+    def test_obsolete_message(self):
+        buf = StringIO(r'''# This is an obsolete message
+#~ msgid "foo"
+#~ msgstr "Voh"
+
+# This message is not obsolete
+#: main.py:1
+msgid "bar"
+msgstr "Bahr"
+''')
+        catalog = pofile.read_po(buf)
+        self.assertEqual(1, len(catalog))
+        self.assertEqual(1, len(catalog.obsolete))
+        message = catalog.obsolete[u'foo']
+        self.assertEqual(u'foo', message.id)
+        self.assertEqual(u'Voh', message.string)
+        self.assertEqual(['This is an obsolete message'], message.user_comments)
+
+    def test_obsolete_message_ignored(self):
+        buf = StringIO(r'''# This is an obsolete message
+#~ msgid "foo"
+#~ msgstr "Voh"
+
+# This message is not obsolete
+#: main.py:1
+msgid "bar"
+msgstr "Bahr"
+''')
+        catalog = pofile.read_po(buf, ignore_obsolete=True)
+        self.assertEqual(1, len(catalog))
+        self.assertEqual(0, len(catalog.obsolete))
+
+    def test_with_context(self):
+        buf = StringIO(r'''# Some string in the menu
+#: main.py:1
+msgctxt "Menu"
+msgid "foo"
+msgstr "Voh"
+
+# Another string in the menu
+#: main.py:2
+msgctxt "Menu"
+msgid "bar"
+msgstr "Bahr"
+''')
+        catalog = pofile.read_po(buf, ignore_obsolete=True)
+        self.assertEqual(2, len(catalog))
+        message = catalog.get('foo', context='Menu')
+        self.assertEqual('Menu', message.context)
+        message = catalog.get('bar', context='Menu')
+        self.assertEqual('Menu', message.context)
+
+        # And verify it pass through write_po
+        out_buf = StringIO()
+        pofile.write_po(out_buf, catalog, omit_header=True)
+        assert out_buf.getvalue().strip() == buf.getvalue().strip(), \
+                                                            out_buf.getvalue()
+
+    def test_with_context_two(self):
+        buf = StringIO(r'''msgctxt "Menu"
+msgid "foo"
+msgstr "Voh"
+
+msgctxt "Mannu"
+msgid "bar"
+msgstr "Bahr"
+''')
+        catalog = pofile.read_po(buf, ignore_obsolete=True)
+        self.assertEqual(2, len(catalog))
+        message = catalog.get('foo', context='Menu')
+        self.assertEqual('Menu', message.context)
+        message = catalog.get('bar', context='Mannu')
+        self.assertEqual('Mannu', message.context)
+        
+        # And verify it pass through write_po
+        out_buf = StringIO()
+        pofile.write_po(out_buf, catalog, omit_header=True)
+        assert out_buf.getvalue().strip() == buf.getvalue().strip(), out_buf.getvalue()
+
+    def test_single_plural_form(self):
+        buf = StringIO(r'''msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"''')
+        catalog = pofile.read_po(buf, locale='ja_JP')
+        self.assertEqual(1, len(catalog))
+        self.assertEqual(1, catalog.num_plurals)
+        message = catalog['foo']
+        self.assertEqual(1, len(message.string))
+
+    def test_singular_plural_form(self):
+        buf = StringIO(r'''msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"
+msgstr[1] "Vohs"''')
+        catalog = pofile.read_po(buf, locale='nl_NL')
+        self.assertEqual(1, len(catalog))
+        self.assertEqual(2, catalog.num_plurals)
+        message = catalog['foo']
+        self.assertEqual(2, len(message.string))
+
+    def test_more_than_two_plural_forms(self):
+        buf = StringIO(r'''msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"
+msgstr[1] "Vohs"
+msgstr[2] "Vohss"''')
+        catalog = pofile.read_po(buf, locale='lv_LV')
+        self.assertEqual(1, len(catalog))
+        self.assertEqual(3, catalog.num_plurals)
+        message = catalog['foo']
+        self.assertEqual(3, len(message.string))
+        self.assertEqual(u'Vohss', message.string[2])
+
+    def test_plural_with_square_brackets(self):
+        buf = StringIO(r'''msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh [text]"
+msgstr[1] "Vohs [text]"''')
+        catalog = pofile.read_po(buf, locale='nb_NO')
+        self.assertEqual(1, len(catalog))
+        self.assertEqual(2, catalog.num_plurals)
+        message = catalog['foo']
+        self.assertEqual(2, len(message.string))
+
+
+class WritePoTestCase(unittest.TestCase):
+
+    def test_join_locations(self):
+        catalog = Catalog()
+        catalog.add(u'foo', locations=[('main.py', 1)])
+        catalog.add(u'foo', locations=[('utils.py', 3)])
+        buf = StringIO()
+        pofile.write_po(buf, catalog, omit_header=True)
+        self.assertEqual('''#: main.py:1 utils.py:3
+msgid "foo"
+msgstr ""''', buf.getvalue().strip())
+
+    def test_duplicate_comments(self):
+        catalog = Catalog()
+        catalog.add(u'foo', auto_comments=['A comment'])
+        catalog.add(u'foo', auto_comments=['A comment'])
+        buf = StringIO()
+        pofile.write_po(buf, catalog, omit_header=True)
+        self.assertEqual('''#. A comment
+msgid "foo"
+msgstr ""''', buf.getvalue().strip())
+
+    def test_wrap_long_lines(self):
+        text = """Here's some text where       
+white space and line breaks matter, and should
+
+not be removed
+
+"""
+        catalog = Catalog()
+        catalog.add(text, locations=[('main.py', 1)])
+        buf = StringIO()
+        pofile.write_po(buf, catalog, no_location=True, omit_header=True,
+                         width=42)
+        self.assertEqual(r'''msgid ""
+"Here's some text where       \n"
+"white space and line breaks matter, and"
+" should\n"
+"\n"
+"not be removed\n"
+"\n"
+msgstr ""''', buf.getvalue().strip())
+
+    def test_wrap_long_lines_with_long_word(self):
+        text = """Here's some text that
+includesareallylongwordthatmightbutshouldnt throw us into an infinite loop
+"""
+        catalog = Catalog()
+        catalog.add(text, locations=[('main.py', 1)])
+        buf = StringIO()
+        pofile.write_po(buf, catalog, no_location=True, omit_header=True,
+                         width=32)
+        self.assertEqual(r'''msgid ""
+"Here's some text that\n"
+"includesareallylongwordthatmightbutshouldnt"
+" throw us into an infinite "
+"loop\n"
+msgstr ""''', buf.getvalue().strip())
+        
+    def test_wrap_long_lines_in_header(self):
+        """
+        Verify that long lines in the header comment are wrapped correctly.
+        """
+        catalog = Catalog(project='AReallyReallyLongNameForAProject',
+                          revision_date=datetime(2007, 4, 1))
+        buf = StringIO()
+        pofile.write_po(buf, catalog)
+        self.assertEqual('''\
+# Translations template for AReallyReallyLongNameForAProject.
+# Copyright (C) 2007 ORGANIZATION
+# This file is distributed under the same license as the
+# AReallyReallyLongNameForAProject project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy''', '\n'.join(buf.getvalue().splitlines()[:7]))
+
+    def test_wrap_locations_with_hyphens(self):
+        catalog = Catalog()
+        catalog.add(u'foo', locations=[
+            ('doupy/templates/base/navmenu.inc.html.py', 60)
+        ])
+        catalog.add(u'foo', locations=[
+            ('doupy/templates/job-offers/helpers.html', 22)
+        ])
+        buf = StringIO()
+        pofile.write_po(buf, catalog, omit_header=True)
+        self.assertEqual('''#: doupy/templates/base/navmenu.inc.html.py:60
+#: doupy/templates/job-offers/helpers.html:22
+msgid "foo"
+msgstr ""''', buf.getvalue().strip())
+        
+    def test_no_wrap_and_width_behaviour_on_comments(self):
+        catalog = Catalog()
+        catalog.add("Pretty dam long message id, which must really be big "
+                    "to test this wrap behaviour, if not it won't work.",
+                    locations=[("fake.py", n) for n in range(1, 30)])
+        buf = StringIO()
+        pofile.write_po(buf, catalog, width=None, omit_header=True)
+        self.assertEqual("""\
+#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7
+#: fake.py:8 fake.py:9 fake.py:10 fake.py:11 fake.py:12 fake.py:13 fake.py:14
+#: fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19 fake.py:20 fake.py:21
+#: fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28
+#: fake.py:29
+msgid "pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't work."
+msgstr ""
+
+""", buf.getvalue().lower())
+        buf = StringIO()
+        pofile.write_po(buf, catalog, width=100, omit_header=True)
+        self.assertEqual("""\
+#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7 fake.py:8 fake.py:9 fake.py:10
+#: fake.py:11 fake.py:12 fake.py:13 fake.py:14 fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19
+#: fake.py:20 fake.py:21 fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28
+#: fake.py:29
+msgid ""
+"pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't"
+" work."
+msgstr ""
+
+""", buf.getvalue().lower())
+
+    def test_pot_with_translator_comments(self):
+        catalog = Catalog()
+        catalog.add(u'foo', locations=[('main.py', 1)],
+                    auto_comments=['Comment About `foo`'])
+        catalog.add(u'bar', locations=[('utils.py', 3)],
+                    user_comments=['Comment About `bar` with',
+                                   'multiple lines.'])
+        buf = StringIO()
+        pofile.write_po(buf, catalog, omit_header=True)
+        self.assertEqual('''#. Comment About `foo`
+#: main.py:1
+msgid "foo"
+msgstr ""
+
+# Comment About `bar` with
+# multiple lines.
+#: utils.py:3
+msgid "bar"
+msgstr ""''', buf.getvalue().strip())
+
+    def test_po_with_obsolete_message(self):
+        catalog = Catalog()
+        catalog.add(u'foo', u'Voh', locations=[('main.py', 1)])
+        catalog.obsolete['bar'] = Message(u'bar', u'Bahr',
+                                          locations=[('utils.py', 3)],
+                                          user_comments=['User comment'])
+        buf = StringIO()
+        pofile.write_po(buf, catalog, omit_header=True)
+        self.assertEqual('''#: main.py:1
+msgid "foo"
+msgstr "Voh"
+
+# User comment
+#~ msgid "bar"
+#~ msgstr "Bahr"''', buf.getvalue().strip())
+
+    def test_po_with_multiline_obsolete_message(self):
+        catalog = Catalog()
+        catalog.add(u'foo', u'Voh', locations=[('main.py', 1)])
+        msgid = r"""Here's a message that covers
+multiple lines, and should still be handled
+correctly.
+"""
+        msgstr = r"""Here's a message that covers
+multiple lines, and should still be handled
+correctly.
+"""
+        catalog.obsolete[msgid] = Message(msgid, msgstr,
+                                          locations=[('utils.py', 3)])
+        buf = StringIO()
+        pofile.write_po(buf, catalog, omit_header=True)
+        self.assertEqual(r'''#: main.py:1
+msgid "foo"
+msgstr "Voh"
+
+#~ msgid ""
+#~ "Here's a message that covers\n"
+#~ "multiple lines, and should still be handled\n"
+#~ "correctly.\n"
+#~ msgstr ""
+#~ "Here's a message that covers\n"
+#~ "multiple lines, and should still be handled\n"
+#~ "correctly.\n"''', buf.getvalue().strip())
+
+    def test_po_with_obsolete_message_ignored(self):
+        catalog = Catalog()
+        catalog.add(u'foo', u'Voh', locations=[('main.py', 1)])
+        catalog.obsolete['bar'] = Message(u'bar', u'Bahr',
+                                          locations=[('utils.py', 3)],
+                                          user_comments=['User comment'])
+        buf = StringIO()
+        pofile.write_po(buf, catalog, omit_header=True, ignore_obsolete=True)
+        self.assertEqual('''#: main.py:1
+msgid "foo"
+msgstr "Voh"''', buf.getvalue().strip())
+
+    def test_po_with_previous_msgid(self):
+        catalog = Catalog()
+        catalog.add(u'foo', u'Voh', locations=[('main.py', 1)],
+                    previous_id=u'fo')
+        buf = StringIO()
+        pofile.write_po(buf, catalog, omit_header=True, include_previous=True)
+        self.assertEqual('''#: main.py:1
+#| msgid "fo"
+msgid "foo"
+msgstr "Voh"''', buf.getvalue().strip())
+
+    def test_po_with_previous_msgid_plural(self):
+        catalog = Catalog()
+        catalog.add((u'foo', u'foos'), (u'Voh', u'Voeh'),
+                    locations=[('main.py', 1)], previous_id=(u'fo', u'fos'))
+        buf = StringIO()
+        pofile.write_po(buf, catalog, omit_header=True, include_previous=True)
+        self.assertEqual('''#: main.py:1
+#| msgid "fo"
+#| msgid_plural "fos"
+msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"
+msgstr[1] "Voeh"''', buf.getvalue().strip())
+
+    def test_sorted_po(self):
+        catalog = Catalog()
+        catalog.add(u'bar', locations=[('utils.py', 3)],
+                    user_comments=['Comment About `bar` with',
+                                   'multiple lines.'])
+        catalog.add((u'foo', u'foos'), (u'Voh', u'Voeh'),
+                    locations=[('main.py', 1)])
+        buf = StringIO()
+        pofile.write_po(buf, catalog, sort_output=True)
+        value = buf.getvalue().strip()
+        assert '''\
+# Comment About `bar` with
+# multiple lines.
+#: utils.py:3
+msgid "bar"
+msgstr ""
+
+#: main.py:1
+msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"
+msgstr[1] "Voeh"''' in value
+        assert value.find('msgid ""') < value.find('msgid "bar"') < value.find('msgid "foo"')
+
+    def test_silent_location_fallback(self):
+        buf = StringIO('''\
+#: broken_file.py
+msgid "missing line number"
+msgstr ""
+
+#: broken_file.py:broken_line_number
+msgid "broken line number"
+msgstr ""''')
+        catalog = pofile.read_po(buf)
+        self.assertEqual(catalog['missing line number'].locations, [])
+        self.assertEqual(catalog['broken line number'].locations, [])
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(pofile))
+    suite.addTest(unittest.makeSuite(ReadPoTestCase))
+    suite.addTest(unittest.makeSuite(WritePoTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/numbers.py
@@ -0,0 +1,589 @@
+# -*- 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:
+#  Padding and rounding increments in pattern:
+#  - http://www.unicode.org/reports/tr35/ (Appendix G.6)
+import math
+import re
+try:
+    from decimal import Decimal
+    have_decimal = True
+except ImportError:
+    have_decimal = False
+
+from babel.core import default_locale, Locale
+
+__all__ = ['format_number', 'format_decimal', 'format_currency',
+           'format_percent', 'format_scientific', 'parse_number',
+           'parse_decimal', 'NumberFormatError']
+__docformat__ = 'restructuredtext en'
+
+LC_NUMERIC = default_locale('LC_NUMERIC')
+
+def get_currency_name(currency, locale=LC_NUMERIC):
+    """Return the name used by the locale for the specified currency.
+    
+    >>> get_currency_name('USD', 'en_US')
+    u'US Dollar'
+    
+    :param currency: the currency code
+    :param locale: the `Locale` object or locale identifier
+    :return: the currency symbol
+    :rtype: `unicode`
+    :since: version 0.9.4
+    """
+    return Locale.parse(locale).currencies.get(currency, currency)
+
+def get_currency_symbol(currency, locale=LC_NUMERIC):
+    """Return the symbol used by the locale for the specified currency.
+    
+    >>> get_currency_symbol('USD', 'en_US')
+    u'$'
+    
+    :param currency: the currency code
+    :param locale: the `Locale` object or locale identifier
+    :return: the currency symbol
+    :rtype: `unicode`
+    """
+    return Locale.parse(locale).currency_symbols.get(currency, currency)
+
+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_plus_sign_symbol(locale=LC_NUMERIC):
+    """Return the plus sign symbol used by the current locale.
+    
+    >>> get_plus_sign_symbol('en_US')
+    u'+'
+    
+    :param locale: the `Locale` object or locale identifier
+    :return: the plus sign symbol
+    :rtype: `unicode`
+    """
+    return Locale.parse(locale).number_symbols.get('plusSign', u'+')
+
+def get_minus_sign_symbol(locale=LC_NUMERIC):
+    """Return the plus sign symbol used by the current locale.
+    
+    >>> get_minus_sign_symbol('en_US')
+    u'-'
+    
+    :param locale: the `Locale` object or locale identifier
+    :return: the plus sign symbol
+    :rtype: `unicode`
+    """
+    return Locale.parse(locale).number_symbols.get('minusSign', u'-')
+
+def get_exponential_symbol(locale=LC_NUMERIC):
+    """Return the symbol used by the locale to separate mantissa and exponent.
+    
+    >>> get_exponential_symbol('en_US')
+    u'E'
+    
+    :param locale: the `Locale` object or locale identifier
+    :return: the exponential symbol
+    :rtype: `unicode`
+    """
+    return Locale.parse(locale).number_symbols.get('exponential', u'E')
+
+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):
+    u"""Return the given number formatted for a specific locale.
+    
+    >>> format_number(1099, locale='en_US')
+    u'1,099'
+    >>> format_number(1099, locale='de_DE')
+    u'1.099'
+
+    
+    :param number: the number to format
+    :param locale: the `Locale` object or locale identifier
+    :return: the formatted number
+    :rtype: `unicode`
+    """
+    # Do we really need this one?
+    return format_decimal(number, locale=locale)
+
+def format_decimal(number, format=None, locale=LC_NUMERIC):
+    u"""Return the given decimal number formatted for a specific locale.
+    
+    >>> format_decimal(1.2345, locale='en_US')
+    u'1.234'
+    >>> format_decimal(1.2346, locale='en_US')
+    u'1.235'
+    >>> format_decimal(-1.2346, locale='en_US')
+    u'-1.235'
+    >>> format_decimal(1.2345, locale='sv_SE')
+    u'1,234'
+    >>> format_decimal(1.2345, locale='de')
+    u'1,234'
+
+    The appropriate thousands grouping and the decimal separator are used for
+    each locale:
+    
+    >>> format_decimal(12345.5, locale='en_US')
+    u'12,345.5'
+
+    :param number: the number to format
+    :param format: 
+    :param locale: the `Locale` object or locale identifier
+    :return: the formatted decimal number
+    :rtype: `unicode`
+    """
+    locale = Locale.parse(locale)
+    if not format:
+        format = locale.decimal_formats.get(format)
+    pattern = parse_pattern(format)
+    return pattern.apply(number, locale)
+
+def format_currency(number, currency, format=None, locale=LC_NUMERIC):
+    u"""Return formatted currency value.
+    
+    >>> format_currency(1099.98, 'USD', locale='en_US')
+    u'$1,099.98'
+    >>> format_currency(1099.98, 'USD', locale='es_CO')
+    u'US$\\xa01.099,98'
+    >>> format_currency(1099.98, 'EUR', locale='de_DE')
+    u'1.099,98\\xa0\\u20ac'
+    
+    The pattern can also be specified explicitly:
+    
+    >>> format_currency(1099.98, 'EUR', u'\xa4\xa4 #,##0.00', locale='en_US')
+    u'EUR 1,099.98'
+    
+    :param number: the number to format
+    :param currency: the currency code
+    :param locale: the `Locale` object or locale identifier
+    :return: the formatted currency value
+    :rtype: `unicode`
+    """
+    locale = Locale.parse(locale)
+    if not format:
+        format = locale.currency_formats.get(format)
+    pattern = parse_pattern(format)
+    return pattern.apply(number, locale, currency=currency)
+
+def format_percent(number, format=None, locale=LC_NUMERIC):
+    """Return formatted percent value for a specific locale.
+    
+    >>> format_percent(0.34, locale='en_US')
+    u'34%'
+    >>> format_percent(25.1234, locale='en_US')
+    u'2,512%'
+    >>> format_percent(25.1234, locale='sv_SE')
+    u'2\\xa0512\\xa0%'
+
+    The format pattern can also be specified explicitly:
+    
+    >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US')
+    u'25,123\u2030'
+
+    :param number: the percent number to format
+    :param format: 
+    :param locale: the `Locale` object or locale identifier
+    :return: the formatted percent number
+    :rtype: `unicode`
+    """
+    locale = Locale.parse(locale)
+    if not format:
+        format = locale.percent_formats.get(format)
+    pattern = parse_pattern(format)
+    return pattern.apply(number, locale)
+
+def format_scientific(number, format=None, locale=LC_NUMERIC):
+    """Return value formatted in scientific notation for a specific locale.
+    
+    >>> format_scientific(10000, locale='en_US')
+    u'1E4'
+
+    The format pattern can also be specified explicitly:
+    
+    >>> format_scientific(1234567, u'##0E00', locale='en_US')
+    u'1.23E06'
+
+    :param number: the number to format
+    :param format: 
+    :param locale: the `Locale` object or locale identifier
+    :return: value formatted in scientific notation.
+    :rtype: `unicode`
+    """
+    locale = Locale.parse(locale)
+    if not format:
+        format = locale.scientific_formats.get(format)
+    pattern = parse_pattern(format)
+    return pattern.apply(number, locale)
+
+
+class NumberFormatError(ValueError):
+    """Exception raised when a string cannot be parsed into a number."""
+
+
+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
+    
+    When the given string cannot be parsed, an exception is raised:
+    
+    >>> parse_number('1.099,98', locale='de')
+    Traceback (most recent call last):
+        ...
+    NumberFormatError: '1.099,98' is not a valid number
+    
+    :param string: the string to parse
+    :param locale: the `Locale` object or locale identifier
+    :return: the parsed number
+    :rtype: `long`
+    :raise `NumberFormatError`: if the string can not be converted to a number
+    """
+    try:
+        return long(string.replace(get_group_symbol(locale), ''))
+    except ValueError:
+        raise NumberFormatError('%r is not a valid number' % string)
+
+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')
+    1099.98
+    
+    When the given string cannot be parsed, an exception is raised:
+    
+    >>> parse_decimal('2,109,998', locale='de')
+    Traceback (most recent call last):
+        ...
+    NumberFormatError: '2,109,998' is not a valid decimal number
+    
+    :param string: the string to parse
+    :param locale: the `Locale` object or locale identifier
+    :return: the parsed decimal number
+    :rtype: `float`
+    :raise `NumberFormatError`: if the string can not be converted to a
+                                decimal number
+    """
+    locale = Locale.parse(locale)
+    try:
+        return float(string.replace(get_group_symbol(locale), '')
+                           .replace(get_decimal_symbol(locale), '.'))
+    except ValueError:
+        raise NumberFormatError('%r is not a valid decimal number' % string)
+
+
+PREFIX_END = r'[^0-9@#.,]'
+NUMBER_TOKEN = r'[0-9@#.\-,E+]'
+
+PREFIX_PATTERN = r"(?P<prefix>(?:'[^']*'|%s)*)" % PREFIX_END
+NUMBER_PATTERN = r"(?P<number>%s+)" % NUMBER_TOKEN
+SUFFIX_PATTERN = r"(?P<suffix>.*)"
+
+number_re = re.compile(r"%s%s%s" % (PREFIX_PATTERN, NUMBER_PATTERN,
+                                    SUFFIX_PATTERN))
+
+def split_number(value):
+    """Convert a number into a (intasstring, fractionasstring) tuple"""
+    if have_decimal and isinstance(value, Decimal):
+        text = str(value)
+    else:
+        text = ('%.9f' % value).rstrip('0')
+    if '.' in text:
+        a, b = text.split('.', 1)
+        if b == '0':
+            b = ''
+    else:
+        a, b = text, ''
+    return a, b
+
+def bankersround(value, ndigits=0):
+    """Round a number to a given precision.
+
+    Works like round() except that the round-half-even (banker's rounding)
+    algorithm is used instead of round-half-up.
+
+    >>> bankersround(5.5, 0)
+    6.0
+    >>> bankersround(6.5, 0)
+    6.0
+    >>> bankersround(-6.5, 0)
+    -6.0
+    >>> bankersround(1234.0, -2)
+    1200.0
+    """
+    sign = int(value < 0) and -1 or 1
+    value = abs(value)
+    a, b = split_number(value)
+    digits = a + b
+    add = 0
+    i = len(a) + ndigits
+    if i < 0 or i >= len(digits):
+        pass
+    elif digits[i] > '5':
+        add = 1
+    elif digits[i] == '5' and digits[i-1] in '13579':
+        add = 1
+    scale = 10**ndigits
+    if have_decimal and isinstance(value, Decimal):
+        return Decimal(int(value * scale + add)) / scale * sign
+    else:
+        return float(int(value * scale + add)) / scale * sign
+
+def parse_pattern(pattern):
+    """Parse number format patterns"""
+    if isinstance(pattern, NumberPattern):
+        return pattern
+
+    # Do we have a negative subpattern?
+    if ';' in pattern:
+        pattern, neg_pattern = pattern.split(';', 1)
+        pos_prefix, number, pos_suffix = number_re.search(pattern).groups()
+        neg_prefix, _, neg_suffix = number_re.search(neg_pattern).groups()
+    else:
+        pos_prefix, number, pos_suffix = number_re.search(pattern).groups()
+        neg_prefix = '-' + pos_prefix
+        neg_suffix = pos_suffix
+    if 'E' in number:
+        number, exp = number.split('E', 1)
+    else:
+        exp = None
+    if '@' in number:
+        if '.' in number and '0' in number:
+            raise ValueError('Significant digit patterns can not contain '
+                             '"@" or "0"')
+    if '.' in number:
+        #integer, fraction = number.rsplit('.', 1)
+        # 2.3 compat: this is rsplit
+        parts = number.split('.')
+        integer, fraction = '.'.join(parts[:-1]), parts[-1]
+    else:
+        integer = number
+        fraction = ''
+    min_frac = max_frac = 0
+
+    def parse_precision(p):
+        """Calculate the min and max allowed digits"""
+        min = max = 0
+        for c in p:
+            if c in '@0':
+                min += 1
+                max += 1
+            elif c == '#':
+                max += 1
+            elif c == ',':
+                continue
+            else:
+                break
+        return min, max
+
+    def parse_grouping(p):
+        """Parse primary and secondary digit grouping
+
+        >>> parse_grouping('##')
+        0, 0
+        >>> parse_grouping('#,###')
+        3, 3
+        >>> parse_grouping('#,####,###')
+        3, 4
+        """
+        width = len(p)
+        g1 = p.rfind(',')
+        if g1 == -1:
+            return 1000, 1000
+        g1 = width - g1 - 1
+        g2 = p[:-g1 - 1].rfind(',')
+        if g2 == -1:
+            return g1, g1
+        g2 = width - g1 - g2 - 2
+        return g1, g2
+
+    int_prec = parse_precision(integer)
+    frac_prec = parse_precision(fraction)
+    if exp:
+        frac_prec = parse_precision(integer+fraction)
+        exp_plus = exp.startswith('+')
+        exp = exp.lstrip('+')
+        exp_prec = parse_precision(exp)
+    else:
+        exp_plus = None
+        exp_prec = None
+    grouping = parse_grouping(integer)
+    return NumberPattern(pattern, (pos_prefix, neg_prefix), 
+                         (pos_suffix, neg_suffix), grouping,
+                         int_prec, frac_prec, 
+                         exp_prec, exp_plus)
+
+
+class NumberPattern(object):
+
+    def __init__(self, pattern, prefix, suffix, grouping,
+                 int_prec, frac_prec, exp_prec, exp_plus):
+        self.pattern = pattern
+        self.prefix = prefix
+        self.suffix = suffix
+        self.grouping = grouping
+        self.int_prec = int_prec
+        self.frac_prec = frac_prec
+        self.exp_prec = exp_prec
+        self.exp_plus = exp_plus
+        if '%' in ''.join(self.prefix + self.suffix):
+            self.scale = 100
+        elif u'‰' in ''.join(self.prefix + self.suffix):
+            self.scale = 1000
+        else:
+            self.scale = 1
+
+    def __repr__(self):
+        return '<%s %r>' % (type(self).__name__, self.pattern)
+
+    def apply(self, value, locale, currency=None):
+        value *= self.scale
+        is_negative = int(value < 0)
+        if self.exp_prec: # Scientific notation
+            value = abs(value)
+            if value:
+                exp = int(math.floor(math.log(value, 10)))
+            else:
+                exp = 0
+            # Minimum number of integer digits
+            if self.int_prec[0] == self.int_prec[1]:
+                exp -= self.int_prec[0] - 1
+            # Exponent grouping
+            elif self.int_prec[1]:
+                exp = int(exp) / self.int_prec[1] * self.int_prec[1]
+            if not have_decimal or not isinstance(value, Decimal):
+                value = float(value)
+            if exp < 0:
+                value = value * 10**(-exp)
+            else:
+                value = value / 10**exp
+            exp_sign = ''
+            if exp < 0:
+                exp_sign = get_minus_sign_symbol(locale)
+            elif self.exp_plus:
+                exp_sign = get_plus_sign_symbol(locale)
+            exp = abs(exp)
+            number = u'%s%s%s%s' % \
+                 (self._format_sigdig(value, self.frac_prec[0], 
+                                     self.frac_prec[1]), 
+                  get_exponential_symbol(locale),  exp_sign,
+                  self._format_int(str(exp), self.exp_prec[0],
+                                   self.exp_prec[1], locale))
+        elif '@' in self.pattern: # Is it a siginificant digits pattern?
+            text = self._format_sigdig(abs(value),
+                                      self.int_prec[0],
+                                      self.int_prec[1])
+            if '.' in text:
+                a, b = text.split('.')
+                a = self._format_int(a, 0, 1000, locale)
+                if b:
+                    b = get_decimal_symbol(locale) + b
+                number = a + b
+            else:
+                number = self._format_int(text, 0, 1000, locale)
+        else: # A normal number pattern
+            a, b = split_number(bankersround(abs(value), 
+                                             self.frac_prec[1]))
+            b = b or '0'
+            a = self._format_int(a, self.int_prec[0],
+                                 self.int_prec[1], locale)
+            b = self._format_frac(b, locale)
+            number = a + b
+        retval = u'%s%s%s' % (self.prefix[is_negative], number,
+                                self.suffix[is_negative])
+        if u'¤' in retval:
+            retval = retval.replace(u'¤¤', currency.upper())
+            retval = retval.replace(u'¤', get_currency_symbol(currency, locale))
+        return retval
+
+    def _format_sigdig(self, value, min, max):
+        """Convert value to a string.
+
+        The resulting string will contain between (min, max) number of
+        significant digits.
+        """
+        a, b = split_number(value)
+        ndecimals = len(a)
+        if a == '0' and b != '':
+            ndecimals = 0
+            while b.startswith('0'):
+                b = b[1:]
+                ndecimals -= 1
+        a, b = split_number(bankersround(value, max - ndecimals))
+        digits = len((a + b).lstrip('0'))
+        if not digits:
+            digits = 1
+        # Figure out if we need to add any trailing '0':s
+        if len(a) >= max and a != '0':
+            return a
+        if digits < min:
+            b += ('0' * (min - digits))
+        if b:
+            return '%s.%s' % (a, b)
+        return a
+
+    def _format_int(self, value, min, max, locale):
+        width = len(value)
+        if width < min:
+            value = '0' * (min - width) + value
+        gsize = self.grouping[0]
+        ret = ''
+        symbol = get_group_symbol(locale)
+        while len(value) > gsize:
+            ret = symbol + value[-gsize:] + ret
+            value = value[:-gsize]
+            gsize = self.grouping[1]
+        return value + ret
+
+    def _format_frac(self, value, locale):
+        min, max = self.frac_prec
+        if len(value) < min:
+            value += ('0' * (min - len(value)))
+        if max == 0 or (min == 0 and int(value) == 0):
+            return ''
+        width = len(value)
+        while len(value) > min and value[-1] == '0':
+            value = value[:-1]
+        return get_decimal_symbol(locale) + value
new file mode 100644
--- /dev/null
+++ b/babel3/babel/plural.py
@@ -0,0 +1,447 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008 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/.
+
+"""CLDR Plural support.  See UTS #35.  EXPERIMENTAL"""
+
+import re
+
+from babel.util import frozenset, set
+
+__all__ = ['PluralRule', 'RuleError', 'to_gettext', 'to_javascript',
+           'to_python']
+__docformat__ = 'restructuredtext en'
+
+
+_plural_tags = ('zero', 'one', 'two', 'few', 'many', 'other')
+_fallback_tag = 'other'
+
+
+class PluralRule(object):
+    """Represents a set of language pluralization rules.  The constructor
+    accepts a list of (tag, expr) tuples or a dict of CLDR rules. The
+    resulting object is callable and accepts one parameter with a positive or
+    negative number (both integer and float) for the number that indicates the
+    plural form for a string and returns the tag for the format:
+
+    >>> rule = PluralRule({'one': 'n is 1'})
+    >>> rule(1)
+    'one'
+    >>> rule(2)
+    'other'
+
+    Currently the CLDR defines these tags: zero, one, two, few, many and
+    other where other is an implicit default.  Rules should be mutually
+    exclusive; for a given numeric value, only one rule should apply (i.e.
+    the condition should only be true for one of the plural rule elements.
+    """
+
+    __slots__ = ('abstract', '_func')
+
+    def __init__(self, rules):
+        """Initialize the rule instance.
+
+        :param rules: a list of ``(tag, expr)``) tuples with the rules
+                      conforming to UTS #35 or a dict with the tags as keys
+                      and expressions as values.
+        :raise RuleError: if the expression is malformed
+        """
+        if isinstance(rules, dict):
+            rules = rules.items()
+        found = set()
+        self.abstract = []
+        for key, expr in rules:
+            if key not in _plural_tags:
+                raise ValueError('unknown tag %r' % key)
+            elif key in found:
+                raise ValueError('tag %r defined twice' % key)
+            found.add(key)
+            self.abstract.append((key, _Parser(expr).ast))
+
+    def __repr__(self):
+        rules = self.rules
+        return '<%s %r>' % (
+            type(self).__name__,
+            ', '.join(['%s: %s' % (tag, rules[tag]) for tag in _plural_tags
+                       if tag in rules])
+        )
+
+    def parse(cls, rules):
+        """Create a `PluralRule` instance for the given rules.  If the rules
+        are a `PluralRule` object, that object is returned.
+
+        :param rules: the rules as list or dict, or a `PluralRule` object
+        :return: a corresponding `PluralRule` object
+        :raise Ruleerror: if the expression is malformed
+        """
+        if isinstance(rules, cls):
+            return rules
+        return cls(rules)
+    parse = classmethod(parse)
+
+    def rules(self):
+        """The `PluralRule` as a dict of unicode plural rules.
+        
+        >>> rule = PluralRule({'one': 'n is 1'})
+        >>> rule.rules
+        {'one': 'n is 1'}
+        """
+        _compile = _UnicodeCompiler().compile
+        return dict([(tag, _compile(ast)) for tag, ast in self.abstract])
+    rules = property(rules, doc=rules.__doc__)
+
+    tags = property(lambda x: frozenset([i[0] for i in x.abstract]), doc="""
+        A set of explicitly defined tags in this rule.  The implicit default
+        ``'other'`` rules is not part of this set unless there is an explicit
+        rule for it.""")
+
+    def __getstate__(self):
+        return self.abstract
+
+    def __setstate__(self, abstract):
+        self.abstract = abstract
+
+    def __call__(self, n):
+        if not hasattr(self, '_func'):
+            self._func = to_python(self)
+        return self._func(n)
+
+
+def to_javascript(rule):
+    """Convert a list/dict of rules or a `PluralRule` object into a JavaScript
+    function.  This function depends on no external library:
+
+    >>> to_javascript({'one': 'n is 1'})
+    "(function(n) { return (n == 1) ? 'one' : 'other'; })"
+
+    Implementation detail: The function generated will probably evaluate
+    expressions involved into range operations multiple times.  This has the
+    advantage that external helper functions are not required and is not a
+    big performance hit for these simple calculations.
+
+    :param rule: the rules as list or dict, or a `PluralRule` object
+    :return: a corresponding JavaScript function as `str`
+    :raise RuleError: if the expression is malformed
+    """
+    to_js = _JavaScriptCompiler().compile
+    result = ['(function(n) { return ']
+    for tag, ast in PluralRule.parse(rule).abstract:
+        result.append('%s ? %r : ' % (to_js(ast), tag))
+    result.append('%r; })' % _fallback_tag)
+    return ''.join(result)
+
+
+def to_python(rule):
+    """Convert a list/dict of rules or a `PluralRule` object into a regular
+    Python function.  This is useful in situations where you need a real
+    function and don't are about the actual rule object:
+
+    >>> func = to_python({'one': 'n is 1', 'few': 'n in 2..4'})
+    >>> func(1)
+    'one'
+    >>> func(3)
+    'few'
+
+    :param rule: the rules as list or dict, or a `PluralRule` object
+    :return: a corresponding Python function
+    :raise RuleError: if the expression is malformed
+    """
+    namespace = {
+        'IN':       in_range,
+        'WITHIN':   within_range,
+        'MOD':      cldr_modulo
+    }
+    to_python = _PythonCompiler().compile
+    result = ['def evaluate(n):']
+    for tag, ast in PluralRule.parse(rule).abstract:
+        result.append(' if (%s): return %r' % (to_python(ast), tag))
+    result.append(' return %r' % _fallback_tag)
+    exec '\n'.join(result) in namespace
+    return namespace['evaluate']
+
+
+def to_gettext(rule):
+    """The plural rule as gettext expression.  The gettext expression is
+    technically limited to integers and returns indices rather than tags.
+
+    >>> to_gettext({'one': 'n is 1', 'two': 'n is 2'})
+    'nplurals=3; plural=((n == 2) ? 1 : (n == 1) ? 0 : 2)'
+
+    :param rule: the rules as list or dict, or a `PluralRule` object
+    :return: an equivalent gettext-style plural expression
+    :raise RuleError: if the expression is malformed
+    """
+    rule = PluralRule.parse(rule)
+
+    used_tags = rule.tags | set([_fallback_tag])
+    _compile = _GettextCompiler().compile
+    _get_index = [tag for tag in _plural_tags if tag in used_tags].index
+
+    result = ['nplurals=%d; plural=(' % len(used_tags)]
+    for tag, ast in rule.abstract:
+        result.append('%s ? %d : ' % (_compile(ast), _get_index(tag)))
+    result.append('%d)' % _get_index(_fallback_tag))
+    return ''.join(result)
+
+
+def in_range(num, min, max):
+    """Integer range test.  This is the callback for the "in" operator
+    of the UTS #35 pluralization rule language:
+
+    >>> in_range(1, 1, 3)
+    True
+    >>> in_range(3, 1, 3)
+    True
+    >>> in_range(1.2, 1, 4)
+    False
+    >>> in_range(10, 1, 4)
+    False
+    """
+    return num == int(num) and within_range(num, min, max)
+
+
+def within_range(num, min, max):
+    """Float range test.  This is the callback for the "within" operator
+    of the UTS #35 pluralization rule language:
+
+    >>> within_range(1, 1, 3)
+    True
+    >>> within_range(1.0, 1, 3)
+    True
+    >>> within_range(1.2, 1, 4)
+    True
+    >>> within_range(10, 1, 4)
+    False
+    """
+    return num >= min and num <= max
+
+
+def cldr_modulo(a, b):
+    """Javaish modulo.  This modulo operator returns the value with the sign
+    of the dividend rather than the divisor like Python does:
+
+    >>> cldr_modulo(-3, 5)
+    -3
+    >>> cldr_modulo(-3, -5)
+    -3
+    >>> cldr_modulo(3, 5)
+    3
+    """
+    reverse = 0
+    if a < 0:
+        a *= -1
+        reverse = 1
+    if b < 0:
+        b *= -1
+    rv = a % b
+    if reverse:
+        rv *= -1
+    return rv
+
+
+class RuleError(Exception):
+    """Raised if a rule is malformed."""
+
+
+class _Parser(object):
+    """Internal parser.  This class can translate a single rule into an abstract
+    tree of tuples. It implements the following grammar::
+
+        condition   = and_condition ('or' and_condition)*
+        and_condition = relation ('and' relation)*
+        relation    = is_relation | in_relation | within_relation | 'n' <EOL>
+        is_relation = expr 'is' ('not')? value
+        in_relation = expr ('not')? 'in' range
+        within_relation = expr ('not')? 'within' range
+        expr        = 'n' ('mod' value)?
+        value       = digit+
+        digit       = 0|1|2|3|4|5|6|7|8|9
+        range       = value'..'value
+
+    - Whitespace can occur between or around any of the above tokens.
+    - Rules should be mutually exclusive; for a given numeric value, only one
+      rule should apply (i.e. the condition should only be true for one of
+      the plural rule elements.
+
+    The translator parses the expression on instanciation into an attribute
+    called `ast`.
+    """
+
+    _rules = [
+        (None, re.compile(r'\s+(?u)')),
+        ('word', re.compile(r'\b(and|or|is|(?:with)?in|not|mod|n)\b')),
+        ('value', re.compile(r'\d+')),
+        ('ellipsis', re.compile(r'\.\.'))
+    ]
+
+    def __init__(self, string):
+        string = string.lower()
+        result = []
+        pos = 0
+        end = len(string)
+        while pos < end:
+            for tok, rule in self._rules:
+                match = rule.match(string, pos)
+                if match is not None:
+                    pos = match.end()
+                    if tok:
+                        result.append((tok, match.group()))
+                    break
+            else:
+                raise RuleError('malformed CLDR pluralization rule.  '
+                                'Got unexpected %r' % string[pos])
+        self.tokens = result[::-1]
+
+        self.ast = self.condition()
+        if self.tokens:
+            raise RuleError('Expected end of rule, got %r' %
+                            self.tokens[-1][1])
+
+    def test(self, type, value=None):
+        return self.tokens and self.tokens[-1][0] == type and \
+               (value is None or self.tokens[-1][1] == value)
+
+    def skip(self, type, value=None):
+        if self.test(type, value):
+            return self.tokens.pop()
+
+    def expect(self, type, value=None, term=None):
+        token = self.skip(type, value)
+        if token is not None:
+            return token
+        if term is None:
+            term = repr(value is None and type or value)
+        if not self.tokens:
+            raise RuleError('expected %s but end of rule reached' % term)
+        raise RuleError('expected %s but got %r' % (term, self.tokens[-1][1]))
+
+    def condition(self):
+        op = self.and_condition()
+        while self.skip('word', 'or'):
+            op = 'or', (op, self.and_condition())
+        return op
+
+    def and_condition(self):
+        op = self.relation()
+        while self.skip('word', 'and'):
+            op = 'and', (op, self.relation())
+        return op
+
+    def relation(self):
+        left = self.expr()
+        if self.skip('word', 'is'):
+            return self.skip('word', 'not') and 'isnot' or 'is', \
+                   (left, self.value())
+        negated = self.skip('word', 'not')
+        method = 'in'
+        if self.skip('word', 'within'):
+            method = 'within'
+        else:
+            self.expect('word', 'in', term="'within' or 'in'")
+        rv = 'relation', (method, left, self.range())
+        if negated:
+            rv = 'not', (rv,)
+        return rv
+
+    def range(self):
+        left = self.value()
+        self.expect('ellipsis')
+        return 'range', (left, self.value())
+
+    def expr(self):
+        self.expect('word', 'n')
+        if self.skip('word', 'mod'):
+            return 'mod', (('n', ()), self.value())
+        return 'n', ()
+
+    def value(self):
+        return 'value', (int(self.expect('value')[1]),)
+
+
+def _binary_compiler(tmpl):
+    """Compiler factory for the `_Compiler`."""
+    return lambda self, l, r: tmpl % (self.compile(l), self.compile(r))
+
+
+def _unary_compiler(tmpl):
+    """Compiler factory for the `_Compiler`."""
+    return lambda self, x: tmpl % self.compile(x)
+
+
+class _Compiler(object):
+    """The compilers are able to transform the expressions into multiple
+    output formats.
+    """
+
+    def compile(self, (op, args)):
+        return getattr(self, 'compile_' + op)(*args)
+
+    compile_n = lambda x: 'n'
+    compile_value = lambda x, v: str(v)
+    compile_and = _binary_compiler('(%s && %s)')
+    compile_or = _binary_compiler('(%s || %s)')
+    compile_not = _unary_compiler('(!%s)')
+    compile_mod = _binary_compiler('(%s %% %s)')
+    compile_is = _binary_compiler('(%s == %s)')
+    compile_isnot = _binary_compiler('(%s != %s)')
+
+    def compile_relation(self, method, expr, range):
+        range = '%s, %s' % tuple(map(self.compile, range[1]))
+        return '%s(%s, %s)' % (method.upper(), self.compile(expr), range)
+
+
+class _PythonCompiler(_Compiler):
+    """Compiles an expression to Python."""
+
+    compile_and = _binary_compiler('(%s and %s)')
+    compile_or = _binary_compiler('(%s or %s)')
+    compile_not = _unary_compiler('(not %s)')
+    compile_mod = _binary_compiler('MOD(%s, %s)')
+
+
+class _GettextCompiler(_Compiler):
+    """Compile into a gettext plural expression."""
+
+    def compile_relation(self, method, expr, range):
+        expr = self.compile(expr)
+        min, max = map(self.compile, range[1])
+        return '(%s >= %s && %s <= %s)' % (expr, min, expr, max)
+
+
+class _JavaScriptCompiler(_GettextCompiler):
+    """Compiles the expression to plain of JavaScript."""
+
+    def compile_relation(self, method, expr, range):
+        code = _GettextCompiler.compile_relation(self, method, expr, range)
+        if method == 'in':
+            expr = self.compile(expr)
+            code = '(parseInt(%s) == %s && %s)' % (expr, expr, code)
+        return code
+
+
+class _UnicodeCompiler(_Compiler):
+    """Returns a unicode pluralization rule again."""
+
+    compile_is = _binary_compiler('%s is %s')
+    compile_isnot = _binary_compiler('%s is not %s')
+    compile_and = _binary_compiler('%s and %s')
+    compile_or = _binary_compiler('%s or %s')
+    compile_mod = _binary_compiler('%s mod %s')
+
+    def compile_not(self, relation):
+        return self.compile_relation(negated=True, *relation[1])
+
+    def compile_relation(self, method, expr, range, negated=False):
+        return '%s%s %s %s' % (
+            self.compile(expr), negated and ' not' or '',
+            method, '%s..%s' % tuple(map(self.compile, range[1]))
+        )
new file mode 100644
--- /dev/null
+++ b/babel3/babel/support.py
@@ -0,0 +1,579 @@
+# -*- 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/.
+
+"""Several classes and functions that help with integrating and using Babel
+in applications.
+
+.. note: the code in this module is not used by Babel itself
+"""
+
+from datetime import date, datetime, time, timedelta
+import gettext
+import locale
+
+from babel.core import Locale
+from babel.dates import format_date, format_datetime, format_time, \
+                        format_timedelta, LC_TIME
+from babel.numbers import format_number, format_decimal, format_currency, \
+                          format_percent, format_scientific, LC_NUMERIC
+from babel.util import set, UTC
+
+__all__ = ['Format', 'LazyProxy', 'Translations']
+__docformat__ = 'restructuredtext en'
+
+
+class Format(object):
+    """Wrapper class providing the various date and number formatting functions
+    bound to a specific locale and time-zone.
+    
+    >>> fmt = Format('en_US', UTC)
+    >>> fmt.date(date(2007, 4, 1))
+    u'Apr 1, 2007'
+    >>> fmt.decimal(1.2345)
+    u'1.234'
+    """
+
+    def __init__(self, locale, tzinfo=None):
+        """Initialize the formatter.
+        
+        :param locale: the locale identifier or `Locale` instance
+        :param tzinfo: the time-zone info (a `tzinfo` instance or `None`)
+        """
+        self.locale = Locale.parse(locale)
+        self.tzinfo = tzinfo
+
+    def date(self, date=None, format='medium'):
+        """Return a date formatted according to the given pattern.
+        
+        >>> fmt = Format('en_US')
+        >>> fmt.date(date(2007, 4, 1))
+        u'Apr 1, 2007'
+        
+        :see: `babel.dates.format_date`
+        """
+        return format_date(date, format, locale=self.locale)
+
+    def datetime(self, datetime=None, format='medium'):
+        """Return a date and time formatted according to the given pattern.
+        
+        >>> from pytz import timezone
+        >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern'))
+        >>> fmt.datetime(datetime(2007, 4, 1, 15, 30))
+        u'Apr 1, 2007 11:30:00 AM'
+        
+        :see: `babel.dates.format_datetime`
+        """
+        return format_datetime(datetime, format, tzinfo=self.tzinfo,
+                               locale=self.locale)
+
+    def time(self, time=None, format='medium'):
+        """Return a time formatted according to the given pattern.
+        
+        >>> from pytz import timezone
+        >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern'))
+        >>> fmt.time(datetime(2007, 4, 1, 15, 30))
+        u'11:30:00 AM'
+        
+        :see: `babel.dates.format_time`
+        """
+        return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale)
+
+    def timedelta(self, delta, granularity='second', threshold=.85):
+        """Return a time delta according to the rules of the given locale.
+        
+        >>> fmt = Format('en_US')
+        >>> fmt.timedelta(timedelta(weeks=11))
+        u'3 mths'
+        
+        :see: `babel.dates.format_timedelta`
+        """
+        return format_timedelta(delta, granularity=granularity,
+                                threshold=threshold, locale=self.locale)
+
+    def number(self, number):
+        """Return an integer number formatted for the locale.
+        
+        >>> fmt = Format('en_US')
+        >>> fmt.number(1099)
+        u'1,099'
+        
+        :see: `babel.numbers.format_number`
+        """
+        return format_number(number, locale=self.locale)
+
+    def decimal(self, number, format=None):
+        """Return a decimal number formatted for the locale.
+        
+        >>> fmt = Format('en_US')
+        >>> fmt.decimal(1.2345)
+        u'1.234'
+        
+        :see: `babel.numbers.format_decimal`
+        """
+        return format_decimal(number, format, locale=self.locale)
+
+    def currency(self, number, currency):
+        """Return a number in the given currency formatted for the locale.
+        
+        :see: `babel.numbers.format_currency`
+        """
+        return format_currency(number, currency, locale=self.locale)
+
+    def percent(self, number, format=None):
+        """Return a number formatted as percentage for the locale.
+        
+        >>> fmt = Format('en_US')
+        >>> fmt.percent(0.34)
+        u'34%'
+        
+        :see: `babel.numbers.format_percent`
+        """
+        return format_percent(number, format, locale=self.locale)
+
+    def scientific(self, number):
+        """Return a number formatted using scientific notation for the locale.
+        
+        :see: `babel.numbers.format_scientific`
+        """
+        return format_scientific(number, locale=self.locale)
+
+
+class LazyProxy(object):
+    """Class for proxy objects that delegate to a specified function to evaluate
+    the actual object.
+    
+    >>> def greeting(name='world'):
+    ...     return 'Hello, %s!' % name
+    >>> lazy_greeting = LazyProxy(greeting, name='Joe')
+    >>> print lazy_greeting
+    Hello, Joe!
+    >>> u'  ' + lazy_greeting
+    u'  Hello, Joe!'
+    >>> u'(%s)' % lazy_greeting
+    u'(Hello, Joe!)'
+    
+    This can be used, for example, to implement lazy translation functions that
+    delay the actual translation until the string is actually used. The
+    rationale for such behavior is that the locale of the user may not always
+    be available. In web applications, you only know the locale when processing
+    a request.
+    
+    The proxy implementation attempts to be as complete as possible, so that
+    the lazy objects should mostly work as expected, for example for sorting:
+    
+    >>> greetings = [
+    ...     LazyProxy(greeting, 'world'),
+    ...     LazyProxy(greeting, 'Joe'),
+    ...     LazyProxy(greeting, 'universe'),
+    ... ]
+    >>> greetings.sort()
+    >>> for greeting in greetings:
+    ...     print greeting
+    Hello, Joe!
+    Hello, universe!
+    Hello, world!
+    """
+    __slots__ = ['_func', '_args', '_kwargs', '_value']
+
+    def __init__(self, func, *args, **kwargs):
+        # Avoid triggering our own __setattr__ implementation
+        object.__setattr__(self, '_func', func)
+        object.__setattr__(self, '_args', args)
+        object.__setattr__(self, '_kwargs', kwargs)
+        object.__setattr__(self, '_value', None)
+
+    def value(self):
+        if self._value is None:
+            value = self._func(*self._args, **self._kwargs)
+            object.__setattr__(self, '_value', value)
+        return self._value
+    value = property(value)
+
+    def __contains__(self, key):
+        return key in self.value
+
+    def __nonzero__(self):
+        return bool(self.value)
+
+    def __dir__(self):
+        return dir(self.value)
+
+    def __iter__(self):
+        return iter(self.value)
+
+    def __len__(self):
+        return len(self.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 __lt__(self, other):
+        return self.value < other
+
+    def __le__(self, other):
+        return self.value <= other
+
+    def __eq__(self, other):
+        return self.value == other
+
+    def __ne__(self, other):
+        return self.value != other
+
+    def __gt__(self, other):
+        return self.value > other
+
+    def __ge__(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[key]
+
+    def __getitem__(self, key):
+        return self.value[key]
+
+    def __setitem__(self, key, value):
+        self.value[key] = value
+
+    
+class Translations(gettext.GNUTranslations, object):
+    """An extended translation catalog class."""
+
+    DEFAULT_DOMAIN = 'messages'
+
+    def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN):
+        """Initialize the translations catalog.
+
+        :param fileobj: the file-like object the translation should be read
+                        from
+        """
+        gettext.GNUTranslations.__init__(self, fp=fileobj)
+        self.files = filter(None, [getattr(fileobj, 'name', None)])
+        self.domain = domain
+        self._domains = {}
+
+    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`
+        """
+        if locales is not None:
+            if not isinstance(locales, (list, tuple)):
+                locales = [locales]
+            locales = [str(locale) for locale in locales]
+        if not domain:
+            domain = cls.DEFAULT_DOMAIN
+        filename = gettext.find(domain, dirname, locales)
+        if not filename:
+            return gettext.NullTranslations()
+        return cls(fileobj=open(filename, 'rb'), domain=domain)
+    load = classmethod(load)
+
+    def __repr__(self):
+        return '<%s: "%s">' % (type(self).__name__,
+                               self._info.get('project-id-version'))
+
+    def add(self, translations, merge=True):
+        """Add the given translations to the catalog.
+
+        If the domain of the translations is different than that of the
+        current catalog, they are added as a catalog that is only accessible
+        by the various ``d*gettext`` functions.
+
+        :param translations: the `Translations` instance with the messages to
+                             add
+        :param merge: whether translations for message domains that have
+                      already been added should be merged with the existing
+                      translations
+        :return: the `Translations` instance (``self``) so that `merge` calls
+                 can be easily chained
+        :rtype: `Translations`
+        """
+        domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN)
+        if merge and domain == self.domain:
+            return self.merge(translations)
+
+        existing = self._domains.get(domain)
+        if merge and existing is not None:
+            existing.merge(translations)
+        else:
+            translations.add_fallback(self)
+            self._domains[domain] = translations
+
+        return self
+
+    def merge(self, translations):
+        """Merge the given translations into the catalog.
+
+        Message translations in the specified 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, gettext.GNUTranslations):
+            self._catalog.update(translations._catalog)
+            if isinstance(translations, Translations):
+                self.files.extend(translations.files)
+
+        return self
+
+    def dgettext(self, domain, message):
+        """Like ``gettext()``, but look the message up in the specified
+        domain.
+        """
+        return self._domains.get(domain, self).gettext(message)
+    
+    def ldgettext(self, domain, message):
+        """Like ``lgettext()``, but look the message up in the specified 
+        domain.
+        """ 
+        return self._domains.get(domain, self).lgettext(message)
+    
+    def dugettext(self, domain, message):
+        """Like ``ugettext()``, but look the message up in the specified
+        domain.
+        """
+        return self._domains.get(domain, self).ugettext(message)
+    
+    def dngettext(self, domain, singular, plural, num):
+        """Like ``ngettext()``, but look the message up in the specified
+        domain.
+        """
+        return self._domains.get(domain, self).ngettext(singular, plural, num)
+    
+    def ldngettext(self, domain, singular, plural, num):
+        """Like ``lngettext()``, but look the message up in the specified
+        domain.
+        """
+        return self._domains.get(domain, self).lngettext(singular, plural, num)
+    
+    def dungettext(self, domain, singular, plural, num):
+        """Like ``ungettext()`` but look the message up in the specified
+        domain.
+        """
+        return self._domains.get(domain, self).ungettext(singular, plural, num)
+
+    # Most of the downwards code, until it get's included in stdlib, from:
+    #    http://bugs.python.org/file10036/gettext-pgettext.patch
+    #    
+    # The encoding of a msgctxt and a msgid in a .mo file is
+    # msgctxt + "\x04" + msgid (gettext version >= 0.15)
+    CONTEXT_ENCODING = '%s\x04%s'
+
+    def pgettext(self, context, message):
+        """Look up the `context` and `message` id in the catalog and return the
+        corresponding message string, as an 8-bit string encoded with the
+        catalog's charset encoding, if known.  If there is no entry in the
+        catalog for the `message` id and `context` , and a fallback has been
+        set, the look up is forwarded to the fallback's ``pgettext()``
+        method. Otherwise, the `message` id is returned.
+        """
+        ctxt_msg_id = self.CONTEXT_ENCODING % (context, message)
+        missing = object()
+        tmsg = self._catalog.get(ctxt_msg_id, missing)
+        if tmsg is missing:
+            if self._fallback:
+                return self._fallback.pgettext(context, message)
+            return message
+        # Encode the Unicode tmsg back to an 8-bit string, if possible
+        if self._output_charset:
+            return tmsg.encode(self._output_charset)
+        elif self._charset:
+            return tmsg.encode(self._charset)
+        return tmsg
+
+    def lpgettext(self, context, message):
+        """Equivalent to ``pgettext()``, but the translation is returned in the
+        preferred system encoding, if no other encoding was explicitly set with
+        ``bind_textdomain_codeset()``.
+        """
+        ctxt_msg_id = self.CONTEXT_ENCODING % (context, message)
+        missing = object()
+        tmsg = self._catalog.get(ctxt_msg_id, missing)
+        if tmsg is missing:
+            if self._fallback:
+                return self._fallback.lpgettext(context, message)
+            return message
+        if self._output_charset:
+            return tmsg.encode(self._output_charset)
+        return tmsg.encode(locale.getpreferredencoding())
+
+    def npgettext(self, context, singular, plural, num):
+        """Do a plural-forms lookup of a message id.  `singular` is used as the
+        message id for purposes of lookup in the catalog, while `num` is used to
+        determine which plural form to use.  The returned message string is an
+        8-bit string encoded with the catalog's charset encoding, if known.
+        
+        If the message id for `context` is not found in the catalog, and a
+        fallback is specified, the request is forwarded to the fallback's
+        ``npgettext()`` method.  Otherwise, when ``num`` is 1 ``singular`` is
+        returned, and ``plural`` is returned in all other cases.
+        """
+        ctxt_msg_id = self.CONTEXT_ENCODING % (context, singular)
+        try:
+            tmsg = self._catalog[(ctxt_msg_id, self.plural(num))]
+            if self._output_charset:
+                return tmsg.encode(self._output_charset)
+            elif self._charset:
+                return tmsg.encode(self._charset)
+            return tmsg
+        except KeyError:
+            if self._fallback:
+                return self._fallback.npgettext(context, singular, plural, num)
+            if num == 1:
+                return singular
+            else:
+                return plural
+
+    def lnpgettext(self, context, singular, plural, num):
+        """Equivalent to ``npgettext()``, but the translation is returned in the
+        preferred system encoding, if no other encoding was explicitly set with
+        ``bind_textdomain_codeset()``.
+        """
+        ctxt_msg_id = self.CONTEXT_ENCODING % (context, singular)
+        try:
+            tmsg = self._catalog[(ctxt_msg_id, self.plural(num))]
+            if self._output_charset:
+                return tmsg.encode(self._output_charset)
+            return tmsg.encode(locale.getpreferredencoding())
+        except KeyError:
+            if self._fallback:
+                return self._fallback.lnpgettext(context, singular, plural, num)
+            if num == 1:
+                return singular
+            else:
+                return plural
+
+    def upgettext(self, context, message):
+        """Look up the `context` and `message` id in the catalog and return the
+        corresponding message string, as a Unicode string.  If there is no entry
+        in the catalog for the `message` id and `context`, and a fallback has
+        been set, the look up is forwarded to the fallback's ``upgettext()``
+        method.  Otherwise, the `message` id is returned.
+        """
+        ctxt_message_id = self.CONTEXT_ENCODING % (context, message)
+        missing = object()
+        tmsg = self._catalog.get(ctxt_message_id, missing)
+        if tmsg is missing:
+            if self._fallback:
+                return self._fallback.upgettext(context, message)
+            return unicode(message)
+        return tmsg
+
+    def unpgettext(self, context, singular, plural, num):
+        """Do a plural-forms lookup of a message id.  `singular` is used as the
+        message id for purposes of lookup in the catalog, while `num` is used to
+        determine which plural form to use.  The returned message string is a
+        Unicode string.
+        
+        If the message id for `context` is not found in the catalog, and a
+        fallback is specified, the request is forwarded to the fallback's
+        ``unpgettext()`` method.  Otherwise, when `num` is 1 `singular` is
+        returned, and `plural` is returned in all other cases.
+        """
+        ctxt_message_id = self.CONTEXT_ENCODING % (context, singular)
+        try:
+            tmsg = self._catalog[(ctxt_message_id, self.plural(num))]
+        except KeyError:
+            if self._fallback:
+                return self._fallback.unpgettext(context, singular, plural, num)
+            if num == 1:
+                tmsg = unicode(singular)
+            else:
+                tmsg = unicode(plural)
+        return tmsg
+
+    def dpgettext(self, domain, context, message):
+        """Like `pgettext()`, but look the message up in the specified
+        `domain`.
+        """
+        return self._domains.get(domain, self).pgettext(context, message)
+    
+    def dupgettext(self, domain, context, message):
+        """Like `upgettext()`, but look the message up in the specified
+        `domain`.
+        """
+        return self._domains.get(domain, self).upgettext(context, message)
+
+    def ldpgettext(self, domain, context, message):
+        """Equivalent to ``dpgettext()``, but the translation is returned in the
+        preferred system encoding, if no other encoding was explicitly set with
+        ``bind_textdomain_codeset()``.
+        """
+        return self._domains.get(domain, self).lpgettext(context, message)
+
+    def dnpgettext(self, domain, context, singular, plural, num):
+        """Like ``npgettext``, but look the message up in the specified
+        `domain`.
+        """
+        return self._domains.get(domain, self).npgettext(context, singular,
+                                                         plural, num)
+        
+    def dunpgettext(self, domain, context, singular, plural, num):
+        """Like ``unpgettext``, but look the message up in the specified
+        `domain`.
+        """
+        return self._domains.get(domain, self).unpgettext(context, singular,
+                                                          plural, num)
+
+    def ldnpgettext(self, domain, context, singular, plural, num):
+        """Equivalent to ``dnpgettext()``, but the translation is returned in
+        the preferred system encoding, if no other encoding was explicitly set
+        with ``bind_textdomain_codeset()``.
+        """
+        return self._domains.get(domain, self).lnpgettext(context, singular,
+                                                          plural, num)
+
new file mode 100644
--- /dev/null
+++ b/babel3/babel/tests/__init__.py
@@ -0,0 +1,32 @@
+# -*- 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 unittest
+
+def suite():
+    from babel.tests import core, dates, localedata, numbers, plural, \
+                            support, util
+    from babel.messages import tests as messages
+    suite = unittest.TestSuite()
+    suite.addTest(core.suite())
+    suite.addTest(dates.suite())
+    suite.addTest(localedata.suite())
+    suite.addTest(messages.suite())
+    suite.addTest(numbers.suite())
+    suite.addTest(plural.suite())
+    suite.addTest(support.suite())
+    suite.addTest(util.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/tests/core.py
@@ -0,0 +1,25 @@
+# -*- 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
+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')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/tests/dates.py
@@ -0,0 +1,275 @@
+# -*- 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/.
+
+from datetime import date, datetime, time
+import doctest
+import unittest
+
+from pytz import timezone
+
+from babel import dates
+
+
+class DateTimeFormatTestCase(unittest.TestCase):
+
+    def test_quarter_format(self):
+        d = date(2006, 6, 8)
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('2', fmt['Q'])
+        self.assertEqual('2nd quarter', fmt['QQQQ'])
+        d = date(2006, 12, 31)
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('Q4', fmt['QQQ'])
+
+    def test_month_context(self):
+        d = date(2006, 1, 8)
+        fmt = dates.DateTimeFormat(d, locale='cs_CZ')
+        self.assertEqual('1', fmt['MMM'])
+        fmt = dates.DateTimeFormat(d, locale='cs_CZ')
+        self.assertEqual('1.', fmt['LLL'])
+
+    def test_abbreviated_month_alias(self):
+        d = date(2006, 3, 8)
+        fmt = dates.DateTimeFormat(d, locale='de_DE')
+        self.assertEqual(u'Mär', fmt['LLL'])
+
+    def test_week_of_year_first(self):
+        d = date(2006, 1, 8)
+        fmt = dates.DateTimeFormat(d, locale='de_DE')
+        self.assertEqual('1', fmt['w'])
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('02', fmt['ww'])
+
+    def test_week_of_year_first_with_year(self):
+        d = date(2006, 1, 1)
+        fmt = dates.DateTimeFormat(d, locale='de_DE')
+        self.assertEqual('52', fmt['w'])
+        self.assertEqual('2005', fmt['YYYY'])
+
+    def test_week_of_year_last(self):
+        d = date(2005, 12, 26)
+        fmt = dates.DateTimeFormat(d, locale='de_DE')
+        self.assertEqual('52', fmt['w'])
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('52', fmt['ww'])
+
+    def test_week_of_month_first(self):
+        d = date(2006, 1, 8)
+        fmt = dates.DateTimeFormat(d, locale='de_DE')
+        self.assertEqual('1', fmt['W'])
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('2', fmt['W'])
+
+    def test_week_of_month_last(self):
+        d = date(2006, 1, 29)
+        fmt = dates.DateTimeFormat(d, locale='de_DE')
+        self.assertEqual('4', fmt['W'])
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('5', fmt['W'])
+
+    def test_day_of_year(self):
+        d = date(2007, 4, 1)
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('91', fmt['D'])
+
+    def test_day_of_year_first(self):
+        d = date(2007, 1, 1)
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('001', fmt['DDD'])
+
+    def test_day_of_year_last(self):
+        d = date(2007, 12, 31)
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('365', fmt['DDD'])
+
+    def test_day_of_week_in_month(self):
+        d = date(2007, 4, 15)
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('3', fmt['F'])
+
+    def test_day_of_week_in_month_first(self):
+        d = date(2007, 4, 1)
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('1', fmt['F'])
+
+    def test_day_of_week_in_month_last(self):
+        d = date(2007, 4, 29)
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('5', fmt['F'])
+
+    def test_local_day_of_week(self):
+        d = date(2007, 4, 1) # a sunday
+        fmt = dates.DateTimeFormat(d, locale='de_DE')
+        self.assertEqual('7', fmt['e']) # monday is first day of week
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('01', fmt['ee']) # sunday is first day of week
+        fmt = dates.DateTimeFormat(d, locale='dv_MV')
+        self.assertEqual('03', fmt['ee']) # friday is first day of week
+
+        d = date(2007, 4, 2) # a monday
+        fmt = dates.DateTimeFormat(d, locale='de_DE')
+        self.assertEqual('1', fmt['e']) # monday is first day of week
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('02', fmt['ee']) # sunday is first day of week
+        fmt = dates.DateTimeFormat(d, locale='dv_MV')
+        self.assertEqual('04', fmt['ee']) # friday is first day of week
+
+    def test_local_day_of_week_standalone(self):
+        d = date(2007, 4, 1) # a sunday
+        fmt = dates.DateTimeFormat(d, locale='de_DE')
+        self.assertEqual('7', fmt['c']) # monday is first day of week
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('1', fmt['c']) # sunday is first day of week
+        fmt = dates.DateTimeFormat(d, locale='dv_MV')
+        self.assertEqual('3', fmt['c']) # friday is first day of week
+
+        d = date(2007, 4, 2) # a monday
+        fmt = dates.DateTimeFormat(d, locale='de_DE')
+        self.assertEqual('1', fmt['c']) # monday is first day of week
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('2', fmt['c']) # sunday is first day of week
+        fmt = dates.DateTimeFormat(d, locale='dv_MV')
+        self.assertEqual('4', fmt['c']) # friday is first day of week
+
+    def test_fractional_seconds(self):
+        t = time(15, 30, 12, 34567)
+        fmt = dates.DateTimeFormat(t, locale='en_US')
+        self.assertEqual('3457', fmt['SSSS'])
+
+    def test_fractional_seconds_zero(self):
+        t = time(15, 30, 0)
+        fmt = dates.DateTimeFormat(t, locale='en_US')
+        self.assertEqual('0000', fmt['SSSS'])
+
+    def test_milliseconds_in_day(self):
+        t = time(15, 30, 12, 345000)
+        fmt = dates.DateTimeFormat(t, locale='en_US')
+        self.assertEqual('55812345', fmt['AAAA'])
+
+    def test_milliseconds_in_day_zero(self):
+        d = time(0, 0, 0)
+        fmt = dates.DateTimeFormat(d, locale='en_US')
+        self.assertEqual('0000', fmt['AAAA'])
+
+    def test_timezone_rfc822(self):
+        tz = timezone('Europe/Berlin')
+        t = time(15, 30, tzinfo=tz)
+        fmt = dates.DateTimeFormat(t, locale='de_DE')
+        self.assertEqual('+0100', fmt['Z'])
+
+    def test_timezone_gmt(self):
+        tz = timezone('Europe/Berlin')
+        t = time(15, 30, tzinfo=tz)
+        fmt = dates.DateTimeFormat(t, locale='de_DE')
+        self.assertEqual('GMT+01:00', fmt['ZZZZ'])
+
+    def test_timezone_no_uncommon(self):
+        tz = timezone('Europe/Paris')
+        dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz)
+        fmt = dates.DateTimeFormat(dt, locale='fr_CA')
+        self.assertEqual('France', fmt['v'])
+
+    def test_timezone_with_uncommon(self):
+        tz = timezone('Europe/Paris')
+        dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz)
+        fmt = dates.DateTimeFormat(dt, locale='fr_CA')
+        self.assertEqual('HEC', fmt['V'])
+
+    def test_timezone_location_format(self):
+        tz = timezone('Europe/Paris')
+        dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz)
+        fmt = dates.DateTimeFormat(dt, locale='fr_FR')
+        self.assertEqual('France', fmt['VVVV'])
+
+    def test_timezone_walltime_short(self):
+        tz = timezone('Europe/Paris')
+        t = time(15, 30, tzinfo=tz)
+        fmt = dates.DateTimeFormat(t, locale='fr_FR')
+        self.assertEqual('HEC', fmt['v'])
+
+    def test_timezone_walltime_long(self):
+        tz = timezone('Europe/Paris')
+        t = time(15, 30, tzinfo=tz)
+        fmt = dates.DateTimeFormat(t, locale='fr_FR')
+        self.assertEqual(u'Heure de l\u2019Europe centrale', fmt['vvvv'])
+
+    def test_hour_formatting(self):
+        l = 'en_US'
+        t = time(0, 0, 0)
+        self.assertEqual(dates.format_time(t, 'h a', locale=l), '12 AM')
+        self.assertEqual(dates.format_time(t, 'H', locale=l), '0')
+        self.assertEqual(dates.format_time(t, 'k', locale=l), '24')
+        self.assertEqual(dates.format_time(t, 'K a', locale=l), '0 AM')
+        t = time(12, 0, 0)
+        self.assertEqual(dates.format_time(t, 'h a', locale=l), '12 PM')
+        self.assertEqual(dates.format_time(t, 'H', locale=l), '12')
+        self.assertEqual(dates.format_time(t, 'k', locale=l), '12')
+        self.assertEqual(dates.format_time(t, 'K a', locale=l), '0 PM')
+
+
+class FormatDateTestCase(unittest.TestCase):
+
+    def test_with_time_fields_in_pattern(self):
+        self.assertRaises(AttributeError, dates.format_date, date(2007, 04, 01),
+                          "yyyy-MM-dd HH:mm", locale='en_US')
+
+    def test_with_time_fields_in_pattern_and_datetime_param(self):
+        self.assertRaises(AttributeError, dates.format_date,
+                          datetime(2007, 04, 01, 15, 30),
+                          "yyyy-MM-dd HH:mm", locale='en_US')
+
+
+class FormatTimeTestCase(unittest.TestCase):
+
+    def test_with_naive_datetime_and_tzinfo(self):
+        string = dates.format_time(datetime(2007, 4, 1, 15, 30),
+                                   'long', tzinfo=timezone('US/Eastern'),
+                                   locale='en')
+        self.assertEqual('11:30:00 AM EDT', string)
+
+    def test_with_date_fields_in_pattern(self):
+        self.assertRaises(AttributeError, dates.format_time, date(2007, 04, 01),
+                          "yyyy-MM-dd HH:mm", locale='en_US')
+
+    def test_with_date_fields_in_pattern_and_datetime_param(self):
+        self.assertRaises(AttributeError, dates.format_time,
+                          datetime(2007, 04, 01, 15, 30),
+                          "yyyy-MM-dd HH:mm", locale='en_US')
+
+
+class FormatTimedeltaTestCase(unittest.TestCase):
+
+    def test_zero_seconds(self):
+        string = dates.format_timedelta(timedelta(seconds=0), locale='en')
+        self.assertEqual('0 seconds', string)
+        string = dates.format_timedelta(timedelta(seconds=0),
+                                        granularity='hour', locale='en')
+        self.assertEqual('0 hours', string)
+
+    def test_small_value_with_granularity(self):
+        string = dates.format_timedelta(timedelta(seconds=42),
+                                        granularity='hour', locale='en')
+        self.assertEqual('1 hour', string)
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(dates))
+    suite.addTest(unittest.makeSuite(DateTimeFormatTestCase))
+    suite.addTest(unittest.makeSuite(FormatDateTestCase))
+    suite.addTest(unittest.makeSuite(FormatTimeTestCase))
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/tests/localedata.py
@@ -0,0 +1,73 @@
+# -*- 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
+import unittest
+
+from babel import localedata
+
+
+class MergeResolveTestCase(unittest.TestCase):
+
+    def test_merge_items(self):
+        d = {1: 'foo', 3: 'baz'}
+        localedata.merge(d, {1: 'Foo', 2: 'Bar'})
+        self.assertEqual({1: 'Foo', 2: 'Bar', 3: 'baz'}, d)
+
+    def test_merge_nested_dict(self):
+        d1 = {'x': {'a': 1, 'b': 2, 'c': 3}}
+        d2 = {'x': {'a': 1, 'b': 12, 'd': 14}}
+        localedata.merge(d1, d2)
+        self.assertEqual({
+            'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}
+        }, d1)
+
+    def test_merge_nested_dict_no_overlap(self):
+        d1 = {'x': {'a': 1, 'b': 2}}
+        d2 = {'y': {'a': 11, 'b': 12}}
+        localedata.merge(d1, d2)
+        self.assertEqual({
+            'x': {'a': 1, 'b': 2},
+            'y': {'a': 11, 'b': 12}
+        }, d1)
+
+    def test_merge_with_alias_and_resolve(self):
+        alias = localedata.Alias('x')
+        d1 = {
+            'x': {'a': 1, 'b': 2, 'c': 3},
+            'y': alias
+        }
+        d2 = {
+            'x': {'a': 1, 'b': 12, 'd': 14},
+            'y': {'b': 22, 'e': 25}
+        }
+        localedata.merge(d1, d2)
+        self.assertEqual({
+            'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14},
+            'y': (alias, {'b': 22, 'e': 25})
+        }, d1)
+        d = localedata.LocaleDataDict(d1)
+        self.assertEqual({
+            'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14},
+            'y': {'a': 1, 'b': 22, 'c': 3, 'd': 14, 'e': 25}
+        }, dict(d.items()))
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(localedata))
+    suite.addTest(unittest.makeSuite(MergeResolveTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/tests/numbers.py
@@ -0,0 +1,153 @@
+# -*- 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/.
+
+try:
+    from decimal import Decimal
+    have_decimal = True
+except ImportError:
+    have_decimal = False
+
+import doctest
+import unittest
+
+from babel import numbers
+
+
+class FormatDecimalTestCase(unittest.TestCase):
+
+    def test_patterns(self):
+        self.assertEqual(numbers.format_decimal(12345, '##0', 
+                         locale='en_US'), '12345')
+        self.assertEqual(numbers.format_decimal(6.5, '0.00', locale='sv'), 
+                         '6,50')
+        self.assertEqual(numbers.format_decimal(10.0**20, 
+                                                '#.00', locale='en_US'), 
+                         '100000000000000000000.00')
+
+    def test_subpatterns(self):
+        self.assertEqual(numbers.format_decimal(-12345, '#,##0.##;-#', 
+                         locale='en_US'), '-12,345')
+        self.assertEqual(numbers.format_decimal(-12345, '#,##0.##;(#)', 
+                         locale='en_US'), '(12,345)')
+
+    def test_default_rounding(self):
+        """
+        Testing Round-Half-Even (Banker's rounding)
+        
+        A '5' is rounded to the closest 'even' number
+        """
+        self.assertEqual(numbers.format_decimal(5.5, '0', locale='sv'), '6')
+        self.assertEqual(numbers.format_decimal(6.5, '0', locale='sv'), '6')
+        self.assertEqual(numbers.format_decimal(6.5, '0', locale='sv'), '6')
+        self.assertEqual(numbers.format_decimal(1.2325, locale='sv'), '1,232')
+        self.assertEqual(numbers.format_decimal(1.2335, locale='sv'), '1,234')
+
+    def test_significant_digits(self):
+        """Test significant digits patterns"""
+        self.assertEqual(numbers.format_decimal(123004, '@@',locale='en_US'), 
+                        '120000')
+        self.assertEqual(numbers.format_decimal(1.12, '@', locale='sv'), '1')
+        self.assertEqual(numbers.format_decimal(1.1, '@@', locale='sv'), '1,1')
+        self.assertEqual(numbers.format_decimal(1.1, '@@@@@##', locale='sv'), 
+                         '1,1000')
+        self.assertEqual(numbers.format_decimal(0.0001, '@@@', locale='sv'), 
+                         '0,000100')
+        self.assertEqual(numbers.format_decimal(0.0001234, '@@@', locale='sv'), 
+                         '0,000123')
+        self.assertEqual(numbers.format_decimal(0.0001234, '@@@#',locale='sv'), 
+                         '0,0001234')
+        self.assertEqual(numbers.format_decimal(0.0001234, '@@@#',locale='sv'), 
+                         '0,0001234')
+        self.assertEqual(numbers.format_decimal(0.12345, '@@@',locale='sv'), 
+                         '0,123')
+        self.assertEqual(numbers.format_decimal(3.14159, '@@##',locale='sv'), 
+                         '3,142')
+        self.assertEqual(numbers.format_decimal(1.23004, '@@##',locale='sv'), 
+                         '1,23')
+        self.assertEqual(numbers.format_decimal(1230.04, '@@,@@',locale='en_US'), 
+                         '12,30')
+        self.assertEqual(numbers.format_decimal(123.41, '@@##',locale='en_US'), 
+                         '123.4')
+        self.assertEqual(numbers.format_decimal(1, '@@',locale='en_US'), 
+                         '1.0')
+        self.assertEqual(numbers.format_decimal(0, '@',locale='en_US'), 
+                         '0')
+        self.assertEqual(numbers.format_decimal(0.1, '@',locale='en_US'), 
+                         '0.1')
+        self.assertEqual(numbers.format_decimal(0.1, '@#',locale='en_US'), 
+                         '0.1')
+        self.assertEqual(numbers.format_decimal(0.1, '@@', locale='en_US'), 
+                         '0.10')
+
+    if have_decimal:
+        def test_decimals(self):
+            """Test significant digits patterns"""
+            self.assertEqual(numbers.format_decimal(Decimal('1.2345'), 
+                                                    '#.00', locale='en_US'), 
+                             '1.23')
+            self.assertEqual(numbers.format_decimal(Decimal('1.2345000'), 
+                                                    '#.00', locale='en_US'), 
+                             '1.23')
+            self.assertEqual(numbers.format_decimal(Decimal('1.2345000'), 
+                                                    '@@', locale='en_US'), 
+                             '1.2')
+            self.assertEqual(numbers.format_decimal(Decimal('12345678901234567890.12345'), 
+                                                    '#.00', locale='en_US'), 
+                             '12345678901234567890.12')
+
+    def test_scientific_notation(self):
+        fmt = numbers.format_scientific(0.1, '#E0', locale='en_US')
+        self.assertEqual(fmt, '1E-1')
+        fmt = numbers.format_scientific(0.01, '#E0', locale='en_US')
+        self.assertEqual(fmt, '1E-2')
+        fmt = numbers.format_scientific(10, '#E0', locale='en_US')
+        self.assertEqual(fmt, '1E1')
+        fmt = numbers.format_scientific(1234, '0.###E0', locale='en_US')
+        self.assertEqual(fmt, '1.234E3')
+        fmt = numbers.format_scientific(1234, '0.#E0', locale='en_US')
+        self.assertEqual(fmt, '1.2E3')
+        # Exponent grouping
+        fmt = numbers.format_scientific(12345, '##0.####E0', locale='en_US')
+        self.assertEqual(fmt, '12.345E3')
+        # Minimum number of int digits
+        fmt = numbers.format_scientific(12345, '00.###E0', locale='en_US')
+        self.assertEqual(fmt, '12.345E3')
+        fmt = numbers.format_scientific(-12345.6, '00.###E0', locale='en_US')
+        self.assertEqual(fmt, '-12.346E3')
+        fmt = numbers.format_scientific(-0.01234, '00.###E0', locale='en_US')
+        self.assertEqual(fmt, '-12.34E-3')
+        # Custom pattern suffic
+        fmt = numbers.format_scientific(123.45, '#.##E0 m/s', locale='en_US')
+        self.assertEqual(fmt, '1.23E2 m/s')
+        # Exponent patterns
+        fmt = numbers.format_scientific(123.45, '#.##E00 m/s', locale='en_US')
+        self.assertEqual(fmt, '1.23E02 m/s')
+        fmt = numbers.format_scientific(0.012345, '#.##E00 m/s', locale='en_US')
+        self.assertEqual(fmt, '1.23E-02 m/s')
+        if have_decimal:
+            fmt = numbers.format_scientific(Decimal('12345'), '#.##E+00 m/s', 
+            locale='en_US')
+            self.assertEqual(fmt, '1.23E+04 m/s')
+        # 0 (see ticket #99)
+        fmt = numbers.format_scientific(0, '#E0', locale='en_US')
+        self.assertEqual(fmt, '0E0')
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(numbers))
+    suite.addTest(unittest.makeSuite(FormatDecimalTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/tests/plural.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008 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 plural
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(plural))
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/tests/support.py
@@ -0,0 +1,173 @@
+# -*- 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
+import os
+from StringIO import StringIO
+import unittest
+
+from babel import support
+from babel.messages import Catalog
+from babel.messages.mofile import write_mo
+
+class TranslationsTestCase(unittest.TestCase):
+    
+    def setUp(self):
+        # Use a locale which won't fail to run the tests
+        os.environ['LANG'] = 'en_US.UTF-8'
+        messages1 = [
+            ('foo', {'string': 'Voh'}),
+            ('foo', {'string': 'VohCTX', 'context': 'foo'}),
+            (('foo1', 'foos1'), {'string': ('Voh1', 'Vohs1')}),
+            (('foo1', 'foos1'), {'string': ('VohCTX1', 'VohsCTX1'), 'context': 'foo'}),
+        ]
+        messages2 = [
+            ('foo', {'string': 'VohD'}),
+            ('foo', {'string': 'VohCTXD', 'context': 'foo'}),
+            (('foo1', 'foos1'), {'string': ('VohD1', 'VohsD1')}),
+            (('foo1', 'foos1'), {'string': ('VohCTXD1', 'VohsCTXD1'), 'context': 'foo'}),
+        ]
+        catalog1 = Catalog(locale='en_GB', domain='messages')
+        catalog2 = Catalog(locale='en_GB', domain='messages1')
+        for ids, kwargs in messages1:
+            catalog1.add(ids, **kwargs)            
+        for ids, kwargs in messages2:
+            catalog2.add(ids, **kwargs)
+        catalog1_fp = StringIO()
+        catalog2_fp = StringIO()
+        write_mo(catalog1_fp, catalog1)
+        catalog1_fp.seek(0)
+        write_mo(catalog2_fp, catalog2)
+        catalog2_fp.seek(0)
+        translations1 = support.Translations(catalog1_fp)
+        translations2 = support.Translations(catalog2_fp, domain='messages1')
+        self.translations = translations1.add(translations2, merge=False)
+
+    def assertEqualTypeToo(self, expected, result):
+        self.assertEqual(expected, result)
+        assert type(expected) == type(result), "instance type's do not " + \
+            "match: %r!=%r" % (type(expected), type(result))
+
+    def test_pgettext(self):
+        self.assertEqualTypeToo('Voh', self.translations.gettext('foo'))
+        self.assertEqualTypeToo('VohCTX', self.translations.pgettext('foo',
+                                                                     'foo'))
+
+    def test_upgettext(self):
+        self.assertEqualTypeToo(u'Voh', self.translations.ugettext('foo'))
+        self.assertEqualTypeToo(u'VohCTX', self.translations.upgettext('foo',
+                                                                       'foo'))
+
+    def test_lpgettext(self):
+        self.assertEqualTypeToo('Voh', self.translations.lgettext('foo'))
+        self.assertEqualTypeToo('VohCTX', self.translations.lpgettext('foo',
+                                                                      'foo'))
+
+    def test_npgettext(self):
+        self.assertEqualTypeToo('Voh1',
+                                self.translations.ngettext('foo1', 'foos1', 1))
+        self.assertEqualTypeToo('Vohs1',
+                                self.translations.ngettext('foo1', 'foos1', 2))
+        self.assertEqualTypeToo('VohCTX1',
+                                self.translations.npgettext('foo', 'foo1',
+                                                            'foos1', 1))
+        self.assertEqualTypeToo('VohsCTX1',
+                                self.translations.npgettext('foo', 'foo1',
+                                                            'foos1', 2))
+
+    def test_unpgettext(self):
+        self.assertEqualTypeToo(u'Voh1',
+                                self.translations.ungettext('foo1', 'foos1', 1))
+        self.assertEqualTypeToo(u'Vohs1',
+                                self.translations.ungettext('foo1', 'foos1', 2))
+        self.assertEqualTypeToo(u'VohCTX1',
+                                self.translations.unpgettext('foo', 'foo1',
+                                                             'foos1', 1))
+        self.assertEqualTypeToo(u'VohsCTX1',
+                                self.translations.unpgettext('foo', 'foo1',
+                                                             'foos1', 2))
+
+    def test_lnpgettext(self):
+        self.assertEqualTypeToo('Voh1',
+                                self.translations.lngettext('foo1', 'foos1', 1))
+        self.assertEqualTypeToo('Vohs1',
+                                self.translations.lngettext('foo1', 'foos1', 2))
+        self.assertEqualTypeToo('VohCTX1',
+                                self.translations.lnpgettext('foo', 'foo1',
+                                                             'foos1', 1))
+        self.assertEqualTypeToo('VohsCTX1',
+                                self.translations.lnpgettext('foo', 'foo1',
+                                                             'foos1', 2))
+
+    def test_dpgettext(self):
+        self.assertEqualTypeToo(
+            'VohD', self.translations.dgettext('messages1', 'foo'))
+        self.assertEqualTypeToo(
+            'VohCTXD', self.translations.dpgettext('messages1', 'foo', 'foo'))
+
+    def test_dupgettext(self):
+        self.assertEqualTypeToo(
+            u'VohD', self.translations.dugettext('messages1', 'foo'))
+        self.assertEqualTypeToo(
+            u'VohCTXD', self.translations.dupgettext('messages1', 'foo', 'foo'))
+
+    def test_ldpgettext(self):
+        self.assertEqualTypeToo(
+            'VohD', self.translations.ldgettext('messages1', 'foo'))
+        self.assertEqualTypeToo(
+            'VohCTXD', self.translations.ldpgettext('messages1', 'foo', 'foo'))
+
+    def test_dnpgettext(self):
+        self.assertEqualTypeToo(
+            'VohD1', self.translations.dngettext('messages1', 'foo1', 'foos1', 1))
+        self.assertEqualTypeToo(
+            'VohsD1', self.translations.dngettext('messages1', 'foo1', 'foos1', 2))
+        self.assertEqualTypeToo(
+            'VohCTXD1', self.translations.dnpgettext('messages1', 'foo', 'foo1',
+                                                     'foos1', 1))
+        self.assertEqualTypeToo(
+            'VohsCTXD1', self.translations.dnpgettext('messages1', 'foo', 'foo1',
+                                                      'foos1', 2))
+
+    def test_dunpgettext(self):
+        self.assertEqualTypeToo(
+            u'VohD1', self.translations.dungettext('messages1', 'foo1', 'foos1', 1))
+        self.assertEqualTypeToo(
+            u'VohsD1', self.translations.dungettext('messages1', 'foo1', 'foos1', 2))
+        self.assertEqualTypeToo(
+            u'VohCTXD1', self.translations.dunpgettext('messages1', 'foo', 'foo1',
+                                                       'foos1', 1))
+        self.assertEqualTypeToo(
+            u'VohsCTXD1', self.translations.dunpgettext('messages1', 'foo', 'foo1',
+                                                        'foos1', 2))
+
+    def test_ldnpgettext(self):
+        self.assertEqualTypeToo(
+            'VohD1', self.translations.ldngettext('messages1', 'foo1', 'foos1', 1))
+        self.assertEqualTypeToo(
+            'VohsD1', self.translations.ldngettext('messages1', 'foo1', 'foos1', 2))
+        self.assertEqualTypeToo(
+            'VohCTXD1', self.translations.ldnpgettext('messages1', 'foo', 'foo1',
+                                                      'foos1', 1))
+        self.assertEqualTypeToo(
+            'VohsCTXD1', self.translations.ldnpgettext('messages1', 'foo', 'foo1',
+                                                       'foos1', 2))
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(support))
+    suite.addTest(unittest.makeSuite(TranslationsTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/tests/util.py
@@ -0,0 +1,25 @@
+# -*- 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
+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')
new file mode 100644
--- /dev/null
+++ b/babel3/babel/util.py
@@ -0,0 +1,356 @@
+# -*- 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 codecs
+from datetime import timedelta, tzinfo
+import os
+import re
+import textwrap
+import time
+from itertools import izip, imap
+try:
+    # assigned so they're importable
+    frozenset = frozenset
+    set = set
+except NameError:
+    from sets import ImmutableSet as frozenset, Set as set
+try:
+    from operator import itemgetter
+except ImportError:
+    def itemgetter(item):
+        return lambda obj: obj[item]
+
+missing = object()
+
+__all__ = ['distinct', 'pathmatch', 'relpath', 'wraptext', 'odict', 'UTC',
+           'LOCALTZ']
+__docformat__ = 'restructuredtext en'
+
+
+def distinct(iterable):
+    """Yield all items in an iterable collection that are distinct.
+
+    Unlike when using sets for a similar effect, the original ordering of the
+    items in the collection is preserved by this function.
+
+    >>> print list(distinct([1, 2, 1, 3, 4, 4]))
+    [1, 2, 3, 4]
+    >>> print list(distinct('foobar'))
+    ['f', 'o', 'b', 'a', 'r']
+
+    :param iterable: the iterable collection providing the data
+    :return: the distinct items in the collection
+    :rtype: ``iterator``
+    """
+    seen = set()
+    for item in iter(iterable):
+        if item not in seen:
+            yield item
+            seen.add(item)
+
+# Regexp to match python magic encoding line
+PYTHON_MAGIC_COMMENT_re = re.compile(
+    r'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE)
+def parse_encoding(fp):
+    """Deduce the encoding of a source file from magic comment.
+
+    It does this in the same way as the `Python interpreter`__
+
+    .. __: http://docs.python.org/ref/encodings.html
+
+    The ``fp`` argument should be a seekable file object.
+
+    (From Jeff Dairiki)
+    """
+    pos = fp.tell()
+    fp.seek(0)
+    try:
+        line1 = fp.readline()
+        has_bom = line1.startswith(codecs.BOM_UTF8)
+        if has_bom:
+            line1 = line1[len(codecs.BOM_UTF8):]
+
+        m = PYTHON_MAGIC_COMMENT_re.match(line1)
+        if not m:
+            try:
+                import parser
+                parser.suite(line1)
+            except (ImportError, SyntaxError):
+                # Either it's a real syntax error, in which case the source is
+                # not valid python source, or line2 is a continuation of line1,
+                # in which case we don't want to scan line2 for a magic
+                # comment.
+                pass
+            else:
+                line2 = fp.readline()
+                m = PYTHON_MAGIC_COMMENT_re.match(line2)
+
+        if has_bom:
+            if m:
+                raise SyntaxError(
+                    "python refuses to compile code with both a UTF8 "
+                    "byte-order-mark and a magic encoding comment")
+            return 'utf_8'
+        elif m:
+            return m.group(1)
+        else:
+            return None
+    finally:
+        fp.seek(pos)
+
+def pathmatch(pattern, filename):
+    """Extended pathname pattern matching.
+    
+    This function is similar to what is provided by the ``fnmatch`` module in
+    the Python standard library, but:
+    
+     * can match complete (relative or absolute) path names, and not just file
+       names, and
+     * also supports a convenience pattern ("**") to match files at any
+       directory level.
+    
+    Examples:
+    
+    >>> pathmatch('**.py', 'bar.py')
+    True
+    >>> pathmatch('**.py', 'foo/bar/baz.py')
+    True
+    >>> pathmatch('**.py', 'templates/index.html')
+    False
+    
+    >>> pathmatch('**/templates/*.html', 'templates/index.html')
+    True
+    >>> pathmatch('**/templates/*.html', 'templates/foo/bar.html')
+    False
+    
+    :param pattern: the glob pattern
+    :param filename: the path name of the file to match against
+    :return: `True` if the path name matches the pattern, `False` otherwise
+    :rtype: `bool`
+    """
+    symbols = {
+        '?':   '[^/]',
+        '?/':  '[^/]/',
+        '*':   '[^/]+',
+        '*/':  '[^/]+/',
+        '**/': '(?:.+/)*?',
+        '**':  '(?:.+/)*?[^/]+',
+    }
+    buf = []
+    for idx, part in enumerate(re.split('([?*]+/?)', pattern)):
+        if idx % 2:
+            buf.append(symbols[part])
+        elif part:
+            buf.append(re.escape(part))
+    match = re.match(''.join(buf) + '$', filename.replace(os.sep, '/'))
+    return match is not None
+
+
+class TextWrapper(textwrap.TextWrapper):
+    wordsep_re = re.compile(
+        r'(\s+|'                                  # any whitespace
+        r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))'    # em-dash
+    )
+
+
+def wraptext(text, width=70, initial_indent='', subsequent_indent=''):
+    """Simple wrapper around the ``textwrap.wrap`` function in the standard
+    library. This version does not wrap lines on hyphens in words.
+    
+    :param text: the text to wrap
+    :param width: the maximum line width
+    :param initial_indent: string that will be prepended to the first line of
+                           wrapped output
+    :param subsequent_indent: string that will be prepended to all lines save
+                              the first of wrapped output
+    :return: a list of lines
+    :rtype: `list`
+    """
+    wrapper = TextWrapper(width=width, initial_indent=initial_indent,
+                          subsequent_indent=subsequent_indent,
+                          break_long_words=False)
+    return wrapper.wrap(text)
+
+
+class odict(dict):
+    """Ordered dict implementation.
+    
+    :see: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747
+    """
+    def __init__(self, data=None):
+        dict.__init__(self, data or {})
+        self._keys = dict.keys(self)
+
+    def __delitem__(self, key):
+        dict.__delitem__(self, key)
+        self._keys.remove(key)
+
+    def __setitem__(self, key, item):
+        dict.__setitem__(self, key, item)
+        if key not in self._keys:
+            self._keys.append(key)
+
+    def __iter__(self):
+        return iter(self._keys)
+    iterkeys = __iter__
+
+    def clear(self):
+        dict.clear(self)
+        self._keys = []
+
+    def copy(self):
+        d = odict()
+        d.update(self)
+        return d
+
+    def items(self):
+        return zip(self._keys, self.values())
+
+    def iteritems(self):
+        return izip(self._keys, self.itervalues())
+
+    def keys(self):
+        return self._keys[:]
+
+    def pop(self, key, default=missing):
+        if default is missing:
+            return dict.pop(self, key)
+        elif key not in self:
+            return default
+        self._keys.remove(key)
+        return dict.pop(self, key, default)
+
+    def popitem(self, key):
+        self._keys.remove(key)
+        return dict.popitem(key)
+
+    def setdefault(self, key, failobj = None):
+        dict.setdefault(self, key, failobj)
+        if key not in self._keys:
+            self._keys.append(key)
+
+    def update(self, dict):
+        for (key, val) in dict.items():
+            self[key] = val
+
+    def values(self):
+        return map(self.get, self._keys)
+
+    def itervalues(self):
+        return imap(self.get, self._keys)
+
+
+try:
+    relpath = os.path.relpath
+except AttributeError:
+    def relpath(path, start='.'):
+        """Compute the relative path to one path from another.
+        
+        >>> relpath('foo/bar.txt', '').replace(os.sep, '/')
+        'foo/bar.txt'
+        >>> relpath('foo/bar.txt', 'foo').replace(os.sep, '/')
+        'bar.txt'
+        >>> relpath('foo/bar.txt', 'baz').replace(os.sep, '/')
+        '../foo/bar.txt'
+        
+        :return: the relative path
+        :rtype: `basestring`
+        """
+        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)
+
+ZERO = timedelta(0)
+
+
+class FixedOffsetTimezone(tzinfo):
+    """Fixed offset in minutes east from UTC."""
+
+    def __init__(self, offset, name=None):
+        self._offset = timedelta(minutes=offset)
+        if name is None:
+            name = 'Etc/GMT+%d' % offset
+        self.zone = name
+
+    def __str__(self):
+        return self.zone
+
+    def __repr__(self):
+        return '<FixedOffset "%s" %s>' % (self.zone, self._offset)
+
+    def utcoffset(self, dt):
+        return self._offset
+
+    def tzname(self, dt):
+        return self.zone
+
+    def dst(self, dt):
+        return ZERO
+
+
+try:
+    from pytz import UTC
+except ImportError:
+    UTC = FixedOffsetTimezone(0, 'UTC')
+    """`tzinfo` object for UTC (Universal Time).
+    
+    :type: `tzinfo`
+    """
+
+STDOFFSET = timedelta(seconds = -time.timezone)
+if time.daylight:
+    DSTOFFSET = timedelta(seconds = -time.altzone)
+else:
+    DSTOFFSET = STDOFFSET
+
+DSTDIFF = DSTOFFSET - STDOFFSET
+
+
+class LocalTimezone(tzinfo):
+
+    def utcoffset(self, dt):
+        if self._isdst(dt):
+            return DSTOFFSET
+        else:
+            return STDOFFSET
+
+    def dst(self, dt):
+        if self._isdst(dt):
+            return DSTDIFF
+        else:
+            return ZERO
+
+    def tzname(self, dt):
+        return time.tzname[self._isdst(dt)]
+
+    def _isdst(self, dt):
+        tt = (dt.year, dt.month, dt.day,
+              dt.hour, dt.minute, dt.second,
+              dt.weekday(), 0, -1)
+        stamp = time.mktime(tt)
+        tt = time.localtime(stamp)
+        return tt.tm_isdst > 0
+
+
+LOCALTZ = LocalTimezone()
+"""`tzinfo` object for local time-zone.
+
+:type: `tzinfo`
+"""
new file mode 100644
--- /dev/null
+++ b/babel3/contrib/babel.js
@@ -0,0 +1,160 @@
+/**
+ * Babel JavaScript Support
+ *
+ * Copyright (C) 2008 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/.
+ */
+
+/**
+ * A simple module that provides a gettext like translation interface.
+ * The catalog passed to load() must be a object conforming to this
+ * interface::
+ *
+ *    {
+ *      messages:     an object of {msgid: translations} items where
+ *                    translations is an array of messages or a single
+ *                    string if the message is not pluralizable.
+ *      plural_expr:  the plural expression for the language.
+ *      locale:       the identifier for this locale.
+ *      domain:       the name of the domain.
+ *    }
+ *
+ * Missing elements in the object are ignored.
+ *
+ * Typical usage::
+ *
+ *    var translations = babel.Translations.load(...).install();
+ */
+var babel = new function() {
+
+  var defaultPluralExpr = function(n) { return n == 1 ? 0 : 1; };
+  var formatRegex = /%?%(?:\(([^\)]+)\))?([disr])/g;
+
+  /**
+   * A translations object implementing the gettext interface
+   */
+  var Translations = this.Translations = function(locale, domain) {
+    this.messages = {};
+    this.locale = locale || 'unknown';
+    this.domain = domain || 'messages';
+    this.pluralexpr = defaultPluralExpr;
+  };
+
+  /**
+   * Create a new translations object from the catalog and return it.
+   * See the babel-module comment for more details.
+   */
+  Translations.load = function(catalog) {
+    var rv = new Translations();
+    rv.load(catalog);
+    return rv;
+  };
+
+  Translations.prototype = {
+    /**
+     * translate a single string.
+     */
+    gettext: function(string) {
+      var translated = this.messages[string];
+      if (typeof translated == 'undefined')
+        return string;
+      return (typeof translated == 'string') ? translated : translated[0];
+    },
+
+    /**
+     * translate a pluralizable string
+     */
+    ngettext: function(singular, plural, n) {
+      var translated = this.messages[singular];
+      if (typeof translated == 'undefined')
+        return (n == 1) ? singular : plural;
+      return translated[this.pluralexpr(n)];
+    },
+
+    /**
+     * Install this translation document wide.  After this call, there are
+     * three new methods on the window object: _, gettext and ngettext
+     */
+    install: function() {
+      var self = this;
+      window._ = window.gettext = function(string) {
+        return self.gettext(string);
+      };
+      window.ngettext = function(singular, plural, n) {
+        return self.ngettext(singular, plural, n);
+      };
+      return this;
+    },
+
+    /**
+     * Works like Translations.load but updates the instance rather
+     * then creating a new one.
+     */
+    load: function(catalog) {
+      if (catalog.messages)
+        this.update(catalog.messages)
+      if (catalog.plural_expr)
+        this.setPluralExpr(catalog.plural_expr);
+      if (catalog.locale)
+        this.locale = catalog.locale;
+      if (catalog.domain)
+        this.domain = catalog.domain;
+      return this;
+    },
+
+    /**
+     * Updates the translations with the object of messages.
+     */
+    update: function(mapping) {
+      for (var key in mapping)
+        if (mapping.hasOwnProperty(key))
+          this.messages[key] = mapping[key];
+      return this;
+    },
+
+    /**
+     * Sets the plural expression
+     */
+    setPluralExpr: function(expr) {
+      this.pluralexpr = new Function('n', 'return +(' + expr + ')');
+      return this;
+    }
+  };
+
+  /**
+   * A python inspired string formatting function.  Supports named and
+   * positional placeholders and "s", "d" and "i" as type characters
+   * without any formatting specifications.
+   *
+   * Examples::
+   *
+   *    babel.format(_('Hello %s'), name)
+   *    babel.format(_('Progress: %(percent)s%%'), {percent: 100})
+   */ 
+  this.format = function() {
+    var arg, string = arguments[0], idx = 0;
+    if (arguments.length == 1)
+      return string;
+    else if (arguments.length == 2 && typeof arguments[1] == 'object')
+      arg = arguments[1];
+    else {
+      arg = [];
+      for (var i = 1, n = arguments.length; i != n; ++i)
+        arg[i - 1] = arguments[i];
+    }
+    return string.replace(formatRegex, function(all, name, type) {
+      if (all[0] == all[1]) return all.substring(1);
+      var value = arg[name || idx++];
+      return (type == 'i' || type == 'd') ? +value : value; 
+    });
+  }
+
+};
new file mode 100644
--- /dev/null
+++ b/babel3/doc/cmdline.txt
@@ -0,0 +1,182 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+======================
+Command-Line Interface
+======================
+
+Babel includes a command-line interface for working with message catalogs,
+similar to the various GNU ``gettext`` tools commonly available on Linux/Unix
+systems.
+
+
+.. contents:: Contents
+   :depth: 2
+.. sectnum::
+
+
+When properly installed, Babel provides a script called ``pybabel``::
+
+    $ pybabel --help 
+    usage: pybabel command [options] [args]
+
+    options:
+      --version       show program's version number and exit
+      -h, --help      show this help message and exit
+      --list-locales  print all known locales and exit
+      -v, --verbose   print as much as possible
+      -q, --quiet     print as little as possible
+
+    commands:
+      compile  compile message catalogs to MO files
+      extract  extract messages from source files and generate a POT file
+      init     create new message catalogs from a POT file
+      update   update existing message catalogs from a POT file
+
+The ``pybabel`` script provides a number of sub-commands that do the actual
+work. Those sub-commands are described below.
+
+
+compile
+=======
+
+The ``compile`` sub-command can be used to compile translation catalogs into
+binary MO files::
+
+    $ pybabel compile --help
+    usage: pybabel compile [options] 
+
+    compile message catalogs to MO files
+
+    options:
+      -h, --help            show this help message and exit
+      -D DOMAIN, --domain=DOMAIN
+                            domain of MO and PO files (default 'messages')
+      -d DIR, --directory=DIR
+                            base directory of catalog files
+      -l LOCALE, --locale=LOCALE
+                            locale of the catalog
+      -i FILE, --input-file=FILE
+                            name of the input file
+      -o FILE, --output-file=FILE
+                            name of the output file (default
+                            '<output_dir>/<locale>/LC_MESSAGES/<domain>.mo')
+      -f, --use-fuzzy       also include fuzzy translations (default False)
+      --statistics          print statistics about translations
+
+If ``directory`` is specified, but ``output-file`` is not, the default filename
+of the output file will be::
+
+    <directory>/<locale>/LC_MESSAGES/<domain>.mo
+
+If neither the ``input_file`` nor the ``locale`` option is set, this command
+looks for all catalog files in the base directory that match the given domain,
+and compiles each of them to MO files in the same directory.
+
+
+extract
+=======
+
+The ``extract`` sub-command can be used to extract localizable messages from
+a collection of source files::
+
+    $ pybabel extract --help
+    usage: pybabel extract [options] dir1 <dir2> ...
+
+    extract messages from source files and generate a POT file
+
+    options:
+      -h, --help            show this help message and exit
+      --charset=CHARSET     charset to use in the output (default "utf-8")
+      -k KEYWORDS, --keyword=KEYWORDS
+                            keywords to look for in addition to the defaults. You
+                            can specify multiple -k flags on the command line.
+      --no-default-keywords
+                            do not include the default keywords
+      -F MAPPING_FILE, --mapping=MAPPING_FILE
+                            path to the extraction mapping file
+      --no-location         do not include location comments with filename and
+                            line number
+      --omit-header         do not include msgid "" entry in header
+      -o OUTPUT, --output=OUTPUT
+                            path to the output POT file
+      -w WIDTH, --width=WIDTH
+                            set output line width (default 76)
+      --no-wrap             do not break long message lines, longer than the
+                            output line width, into several lines
+      --sort-output         generate sorted output (default False)
+      --sort-by-file        sort output by file location (default False)
+      --msgid-bugs-address=EMAIL@ADDRESS
+                            set report address for msgid
+      --copyright-holder=COPYRIGHT_HOLDER
+                            set copyright holder in output
+      -c TAG, --add-comments=TAG
+                            place comment block with TAG (or those preceding
+                            keyword lines) in output file. One TAG per argument
+                            call
+
+
+init
+====
+
+The `init` sub-command creates a new translations catalog based on a PO
+template file::
+
+    $ pybabel init --help
+    usage: pybabel init [options] 
+
+    create new message catalogs from a POT file
+
+    options:
+      -h, --help            show this help message and exit
+      -D DOMAIN, --domain=DOMAIN
+                            domain of PO file (default 'messages')
+      -i FILE, --input-file=FILE
+                            name of the input file
+      -d DIR, --output-dir=DIR
+                            path to output directory
+      -o FILE, --output-file=FILE
+                            name of the output file (default
+                            '<output_dir>/<locale>/LC_MESSAGES/<domain>.po')
+      -l LOCALE, --locale=LOCALE
+                            locale for the new localized catalog
+
+
+update
+======
+
+The `update` sub-command updates an existing new translations catalog based on
+a PO template file::
+
+    $ pybabel update --help
+    usage: pybabel update [options] 
+
+    update existing message catalogs from a POT file
+
+    options:
+      -h, --help            show this help message and exit
+      -D DOMAIN, --domain=DOMAIN
+                            domain of PO file (default 'messages')
+      -i FILE, --input-file=FILE
+                            name of the input file
+      -d DIR, --output-dir=DIR
+                            path to output directory
+      -o FILE, --output-file=FILE
+                            name of the output file (default
+                            '<output_dir>/<locale>/LC_MESSAGES/<domain>.po')
+      -l LOCALE, --locale=LOCALE
+                            locale of the translations catalog
+      --ignore-obsolete     do not include obsolete messages in the output
+                            (default False)
+      -N, --no-fuzzy-matching
+                            do not use fuzzy matching (default False)
+      --previous            keep previous msgids of translated messages (default
+                            False)
+
+If ``output_dir`` is specified, but ``output-file`` is not, the default
+filename of the output file will be::
+
+    <directory>/<locale>/LC_MESSAGES/<domain>.mo
+
+If neither the ``output_file`` nor the ``locale`` option is set, this command
+looks for all catalog files in the base directory that match the given domain,
+and updates each of them.
new file mode 100644
--- /dev/null
+++ b/babel3/doc/dates.txt
@@ -0,0 +1,332 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+===============
+Date Formatting
+===============
+
+
+.. contents:: Contents
+   :depth: 2
+.. sectnum::
+
+
+When working with date and time information in Python, you commonly use the
+classes ``date``, ``datetime`` and/or ``time`` from the `datetime`_ package.
+Babel provides functions for locale-specific formatting of those objects in its
+``dates`` module:
+
+.. _`datetime`: http://docs.python.org/lib/module-datetime.html
+
+.. code-block:: pycon
+
+    >>> from datetime import date, datetime, time
+    >>> from babel.dates import format_date, format_datetime, format_time
+
+    >>> d = date(2007, 4, 1)
+    >>> format_date(d, locale='en')
+    u'Apr 1, 2007'
+    >>> format_date(d, locale='de_DE')
+    u'01.04.2007'
+
+As this example demonstrates, Babel will automatically choose a date format
+that is appropriate for the requested locale.
+
+The ``format_*()`` functions also accept an optional ``format`` argument, which
+allows you to choose between one of four format variations:
+
+ * ``short``,
+ * ``medium`` (the default),
+ * ``long``, and
+ * ``full``.
+
+For example:
+
+.. code-block:: pycon
+
+    >>> format_date(d, format='short', locale='en')
+    u'4/1/07'
+    >>> format_date(d, format='long', locale='en')
+    u'April 1, 2007'
+    >>> format_date(d, format='full', locale='en')
+    u'Sunday, April 1, 2007'
+
+
+Pattern Syntax
+==============
+
+While Babel makes it simple to use the appropriate date/time format for a given
+locale, you can also force it to use custom patterns. Note that Babel uses
+different patterns for specifying number and date formats compared to the
+Python equivalents (such as ``time.strftime()``), which have mostly been
+inherited from C and POSIX. The patterns used in Babel are based on the
+`Locale Data Markup Language specification`_ (LDML), which defines them as
+follows:
+
+    A date/time pattern is a string of characters, where specific strings of
+    characters are replaced with date and time data from a calendar when formatting
+    or used to generate data for a calendar when parsing. […]
+
+    Characters may be used multiple times. For example, if ``y`` is used for the
+    year, ``yy`` might produce "99", whereas ``yyyy`` produces "1999". For most
+    numerical fields, the number of characters specifies the field width. For
+    example, if ``h`` is the hour, ``h`` might produce "5", but ``hh`` produces
+    "05". For some characters, the count specifies whether an abbreviated or full
+    form should be used […]
+
+    Two single quotes represent a literal single quote, either inside or outside
+    single quotes. Text within single quotes is not interpreted in any way (except
+    for two adjacent single quotes).
+
+For example:
+
+.. code-block:: pycon
+
+    >>> d = date(2007, 4, 1)
+    >>> format_date(d, "EEE, MMM d, ''yy", locale='en')
+    u"Sun, Apr 1, '07"
+    >>> format_date(d, "EEEE, d.M.yyyy", locale='de')
+    u'Sonntag, 1.4.2007'
+
+    >>> t = time(15, 30)
+    >>> format_time(t, "hh 'o''clock' a", locale='en')
+    u"03 o'clock PM"
+    >>> format_time(t, 'H:mm a', locale='de')
+    u'15:30 nachm.'
+
+    >>> dt = datetime(2007, 4, 1, 15, 30)
+    >>> format_datetime(dt, "yyyyy.MMMM.dd GGG hh:mm a", locale='en')
+    u'02007.April.01 AD 03:30 PM'
+
+The syntax for custom datetime format patterns is described in detail in the
+the `Locale Data Markup Language specification`_. The following table is just a
+relatively brief overview.
+
+ .. _`Locale Data Markup Language specification`: http://unicode.org/reports/tr35/#Date_Format_Patterns
+
+Date Fields
+-----------
+
+  +----------+--------+--------------------------------------------------------+
+  | Field    | Symbol | Description                                            |
+  +==========+========+========================================================+
+  | Era      | ``G``  | Replaced with the era string for the current date. One |
+  |          |        | to three letters for the abbreviated form, four        |
+  |          |        | lettersfor the long form, five for the narrow form     |
+  +----------+--------+--------------------------------------------------------+
+  | Year     | ``y``  | Replaced by the year. Normally the length specifies    |
+  |          |        | the padding, but for two letters it also specifies the |
+  |          |        | maximum length.                                        |
+  |          +--------+--------------------------------------------------------+
+  |          | ``Y``  | Same as ``y`` but uses the ISO year-week calendar.     |
+  |          +--------+--------------------------------------------------------+
+  |          | ``u``  | ??                                                     |
+  +----------+--------+--------------------------------------------------------+
+  | Quarter  | ``Q``  | Use one or two for the numerical quarter, three for    |
+  |          |        | the abbreviation, or four for the full name.           |
+  |          +--------+--------------------------------------------------------+
+  |          | ``q``  | Use one or two for the numerical quarter, three for    |
+  |          |        | the abbreviation, or four for the full name.           |
+  +----------+--------+--------------------------------------------------------+
+  | Month    | ``M``  | Use one or two for the numerical month, three for the  |
+  |          |        | abbreviation, or four for the full name, or five for   |
+  |          |        | the narrow name.                                       |
+  |          +--------+--------------------------------------------------------+
+  |          | ``L``  | Use one or two for the numerical month, three for the  |
+  |          |        | abbreviation, or four for the full name, or 5 for the  |
+  |          |        | narrow name.                                           |
+  +----------+--------+--------------------------------------------------------+
+  | Week     | ``w``  | Week of year.                                          |
+  |          +--------+--------------------------------------------------------+
+  |          | ``W``  | Week of month.                                         |
+  +----------+--------+--------------------------------------------------------+
+  | Day      | ``d``  | Day of month.                                          |
+  |          +--------+--------------------------------------------------------+
+  |          | ``D``  | Day of year.                                           |
+  |          +--------+--------------------------------------------------------+
+  |          | ``F``  | Day of week in month.                                  |
+  |          +--------+--------------------------------------------------------+
+  |          | ``g``  | ??                                                     |
+  +----------+--------+--------------------------------------------------------+
+  | Week day | ``E``  | Day of week. Use one through three letters for the     |
+  |          |        | short day, or four for the full name, or five for the  |
+  |          |        | narrow name.                                           |
+  |          +--------+--------------------------------------------------------+
+  |          | ``e``  | Local day of week. Same as E except adds a numeric     |
+  |          |        | value that will depend on the local starting day of    |
+  |          |        | the week, using one or two letters.                    |
+  |          +--------+--------------------------------------------------------+
+  |          | ``c``  | ??                                                     |
+  +----------+--------+--------------------------------------------------------+
+
+Time Fields
+-----------
+
+  +----------+--------+--------------------------------------------------------+
+  | Field    | Symbol | Description                                            |
+  +==========+========+========================================================+
+  | Period   | ``a``  | AM or PM                                               |
+  +----------+--------+--------------------------------------------------------+
+  | Hour     | ``h``  | Hour [1-12].                                           |
+  |          +--------+--------------------------------------------------------+
+  |          | ``H``  | Hour [0-23].                                           |
+  |          +--------+--------------------------------------------------------+
+  |          | ``K``  | Hour [0-11].                                           |
+  |          +--------+--------------------------------------------------------+
+  |          | ``k``  | Hour [1-24].                                           |
+  +----------+--------+--------------------------------------------------------+
+  | Minute   | ``m``  | Use one or two for zero places padding.                |
+  +----------+--------+--------------------------------------------------------+
+  | Second   | ``s``  | Use one or two for zero places padding.                |
+  |          +--------+--------------------------------------------------------+
+  |          | ``S``  | Fractional second, rounds to the count of letters.     |
+  |          +--------+--------------------------------------------------------+
+  |          | ``A``  | Milliseconds in day.                                   |
+  +----------+--------+--------------------------------------------------------+ 
+  | Timezone | ``z``  | Use one to three letters for the short timezone or     |
+  |          |        | four for the full name.                                |
+  |          +--------+--------------------------------------------------------+
+  |          | ``Z``  | Use one to three letters for RFC 822, four letters for |
+  |          |        | GMT format.                                            |
+  |          +--------+--------------------------------------------------------+
+  |          | ``v``  | Use one letter for short wall (generic) time, four for |
+  |          |        | long wall time.                                        |
+  |          +--------+--------------------------------------------------------+
+  |          | ``V``  | Same as ``z``, except that timezone abbreviations      |
+  |          |        | should be used regardless of whether they are in       |
+  |          |        | common use by the locale.                              |
+  +----------+--------+--------------------------------------------------------+
+
+
+Time Delta Formatting
+=====================
+
+In addition to providing functions for formatting localized dates and times,
+the ``babel.dates`` module also provides a function to format the difference
+between two times, called a ''time delta''. These are usually represented as
+``datetime.timedelta`` objects in Python, and it's also what you get when you
+subtract one ``datetime`` object from an other.
+
+The ``format_timedelta`` function takes a ``timedelta`` object and returns a
+human-readable representation. This happens at the cost of precision, as it
+chooses only the most significant unit (such as year, week, or hour) of the
+difference, and displays that:
+
+.. code-block:: pycon
+
+    >>> from datetime import timedelta
+    >>> from babel.dates import format_timedelta
+    >>> delta = timedelta(days=6)
+    >>> format_timedelta(delta, locale='en_US')
+    u'1 week'
+
+The resulting strings are based from the CLDR data, and are properly
+pluralized depending on the plural rules of the locale and the calculated
+number of units.
+
+The function provides parameters for you to influence how this most significant
+unit is chosen: with ``threshold`` you set the value after which the
+presentation switches to the next larger unit, and with ``granularity`` you
+can limit the smallest unit to display:
+
+.. code-block:: pycon
+
+    >>> delta = timedelta(days=6)
+    >>> format_timedelta(delta, threshold=1.2, locale='en_US')
+    u'6 days'
+    >>> format_timedelta(delta, granularity='month', locale='en_US')
+    u'1 month'
+
+
+Time-zone Support
+=================
+
+Many of the verbose time formats include the time-zone, but time-zone
+information is not by default available for the Python ``datetime`` and
+``time`` objects. The standard library includes only the abstract ``tzinfo``
+class, which you need appropriate implementations for to actually use in your
+application. Babel includes a ``tzinfo`` implementation for UTC (Universal
+Time).
+
+For real time-zone support, it is strongly recommended that you use the
+third-party package `pytz`_, which includes the definitions of practically all
+of the time-zones used on the world, as well as important functions for
+reliably converting from UTC to local time, and vice versa:
+
+.. code-block:: pycon
+
+    >>> from datetime import time
+    >>> from pytz import timezone, utc
+    >>> dt = datetime(2007, 04, 01, 15, 30, tzinfo=utc)
+    >>> eastern = timezone('US/Eastern')
+    >>> format_datetime(dt, 'H:mm Z', tzinfo=eastern, locale='en_US')
+    u'11:30 -0400'
+
+The recommended approach to deal with different time-zones in a Python
+application is to always use UTC internally, and only convert from/to the users
+time-zone when accepting user input and displaying date/time data, respectively.
+You can use Babel together with ``pytz`` to apply a time-zone to any
+``datetime`` or ``time`` object for display, leaving the original information
+unchanged:
+
+.. code-block:: pycon
+
+    >>> british = timezone('Europe/London')
+    >>> format_datetime(dt, 'H:mm zzzz', tzinfo=british, locale='en_US')
+    u'16:30 British Summer Time'
+
+Here, the given UTC time is adjusted to the "Europe/London" time-zone, and
+daylight savings time is taken into account. Daylight savings time is also
+applied to ``format_time``, but because the actual date is unknown in that
+case, the current day is assumed to determine whether DST or standard time
+should be used.
+
+ .. _`pytz`: http://pytz.sourceforge.net/
+
+
+Localized Time-zone Names
+-------------------------
+
+While the ``Locale`` class provides access to various locale display names
+related to time-zones, the process of building a localized name of a time-zone
+is actually quite complicated. Babel implements it in separately usable
+functions in the ``babel.dates`` module, most importantly the
+``get_timezone_name`` function:
+
+.. code-block:: pycon
+
+    >>> from pytz import timezone
+    >>> from babel import Locale
+    >>> from babel.dates import get_timezone_name
+
+    >>> tz = timezone('Europe/Berlin')
+    >>> get_timezone_name(tz, locale=Locale.parse('pt_PT'))
+    u'Hor\xe1rio Alemanha'
+
+You can pass the function either a ``datetime.tzinfo`` object, or a
+``datetime.date`` or ``datetime.datetime`` object. If you pass an actual date,
+the function will be able to take daylight savings time into account. If you
+pass just the time-zone, Babel does not know whether daylight savings time is
+in effect, so it uses a generic representation, which is useful for example to
+display a list of time-zones to the user.
+
+.. code-block:: pycon
+
+    >>> from datetime import datetime
+
+    >>> dt = tz.localize(datetime(2007, 8, 15))
+    >>> get_timezone_name(dt, locale=Locale.parse('de_DE'))
+    u'Mitteleurop\xe4ische Sommerzeit'
+    >>> get_timezone_name(tz, locale=Locale.parse('de_DE'))
+    u'Deutschland'
+
+
+Parsing Dates
+=============
+
+Babel can also parse date and time information in a locale-sensitive manner:
+
+.. code-block:: pycon
+
+    >>> from babel.dates import parse_date, parse_datetime, parse_time
+
+.. note:: Date/time parsing is not properly implemented yet
new file mode 100644
--- /dev/null
+++ b/babel3/doc/display.txt
@@ -0,0 +1,83 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+====================
+Locale Display Names
+====================
+
+.. contents:: Contents
+   :depth: 2
+.. sectnum::
+
+
+Introduction
+============
+
+While `message catalogs <messages.html>`_ allow you to localize any messages
+in your application, there are a number of strings that are used in many
+applications for which translations are readily available.
+
+Imagine for example you have a list of countries that users can choose from,
+and you'd like to display the names of those countries in the language the
+user prefers. Instead of translating all those country names yourself in your
+application, you can make use of the translations provided by the locale data
+included with Babel, which is based on the `Common Locale Data Repository 
+(CLDR) <http://unicode.org/cldr/>`_ developed and maintained by the `Unicode
+Consortium <http://unicode.org/>`_.
+
+
+The ``Locale`` Class
+====================
+
+You normally access such locale data through the `Locale`_ class provided
+by Babel:
+
+.. code-block:: pycon
+
+    >>> from babel import Locale
+    >>> locale = Locale('en', 'US')
+    >>> locale.territories['US']
+    u'United States'
+    >>> locale = Locale('es', 'MX')
+    >>> locale.territories['US']
+    u'Estados Unidos'
+
+.. _`Locale`: api/babel.core.Locale-class.html
+
+In addition to country/territory names, the locale data also provides access to
+names of languages, scripts, variants, time zones, and more. Some of the data
+is closely related to number and date formatting.
+
+Most of the corresponding ``Locale`` properties return dictionaries, where the
+key is a code such as the ISO country and language codes. Consult the API
+documentation for references to the relevant specifications.
+
+
+Calender Display Names
+======================
+
+The `Locale`_ class provides access to many locale display names related to
+calendar display, such as the names of week days or months.
+
+These display names are of course used for date formatting, but can also be
+used, for example, to show a list of months to the user in their preferred
+language:
+
+.. code-block:: pycon
+
+    >>> locale = Locale('es')
+    >>> month_names = locale.months['format']['wide'].items()
+    >>> month_names.sort()
+    >>> for idx, name in month_names:
+    ...     print name
+    enero
+    febrero
+    marzo
+    abril
+    mayo
+    junio
+    julio
+    agosto
+    septiembre
+    octubre
+    noviembre
+    diciembre
new file mode 100644
--- /dev/null
+++ b/babel3/doc/index.txt
@@ -0,0 +1,30 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+=======
+Preface
+=======
+
+.. image:: logo.png
+   :width: 426
+   :height: 187
+   :align: center
+   :alt: Babel
+   :class: logo
+
+---------------------------------------------------
+Simple Internationalization for Python Applications
+---------------------------------------------------
+
+Babel is an integrated collection of utilities that assist in
+internationalizing and localizing Python applications, with an emphasis on
+web-based applications.
+
+ * `Introduction <intro.html>`_
+ * `Locale Display Names <display.html>`_
+ * `Date Formatting <dates.html>`_
+ * `Number Formatting <numbers.html>`_
+ * `Working with Message Catalogs <messages.html>`_
+ * `Command-Line Interface <cmdline.html>`_
+ * `Distutils/Setuptools Integration <setup.html>`_
+ * `Support Classes and Functions <support.html>`_
+ * `Generated API Documentation <api/index.html>`_
new file mode 100644
--- /dev/null
+++ b/babel3/doc/intro.txt
@@ -0,0 +1,61 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+============
+Introduction
+============
+
+The functionality Babel provides for internationalization (I18n) and
+localization (L10N) can be separated into two different aspects:
+
+ * tools to build and work with ``gettext`` message catalogs, and
+ * a Python interface to the CLDR (Common Locale Data Repository), providing
+   access to various locale display names, localized number and date
+   formatting, etc.
+
+.. contents:: Contents
+   :depth: 2
+.. sectnum::
+
+
+Message Catalogs
+================
+
+While the Python standard library includes a
+`gettext <http://docs.python.org/lib/module-gettext.html>`_ module that enables
+applications to use message catalogs, it requires developers to build these
+catalogs using GNU tools such as ``xgettext``, ``msgmerge``, and ``msgfmt``.
+And while ``xgettext`` does have support for extracting messages from Python
+files, it does not know how to deal with other kinds of files commonly found
+in Python web-applications, such as templates, nor does it provide an easy
+extensibility mechanism to add such support.
+
+Babel addresses this by providing a framework where various extraction methods
+can be plugged in to a larger message extraction framework, and also removes
+the dependency on the GNU ``gettext`` tools for common tasks, as these aren't
+necessarily available on all platforms. See `Working with Message Catalogs`_
+for details on this aspect of Babel.
+
+.. _`Working with Message Catalogs`: messages.html
+
+
+Locale Data
+===========
+
+Furthermore, while the Python standard library does include support for basic
+localization with respect to the formatting of numbers and dates (the
+`locale <http://docs.python.org/lib/module-locale.html>`_ module, among others),
+this support is based on the assumption that there will be only one specific
+locale used per process (at least simultaneously.) Also, it doesn't provide
+access to other kinds of locale data, such as the localized names of countries,
+languages, or time-zones, which are frequently needed in web-based applications.
+
+For these requirements, Babel includes data extracted from the `Common Locale
+Data Repository (CLDR) <http://unicode.org/cldr/>`_, and provides a number of
+convenient methods for accessing and using this data. See `Locale Display
+Names`_, `Date Formatting`_, and `Number Formatting`_ for more information on
+this aspect of Babel.
+
+
+.. _`Locale Display Names`: display.html
+.. _`Date Formatting`: dates.html
+.. _`Number Formatting`: numbers.html
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5385fd067c5f2433249ee2445e466a4ee2274746
GIT binary patch
literal 44986
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8060875c307123012418676deef8e0f8f3a2f327
GIT binary patch
literal 15484
zc$}rX1yEe=+9is+yK4y0I0TpAE)8_!9)e5I;KAM9T^e_HcMYyVg9eA-!>MoT+%y0E
z@2#nsRn@k8J!`G!dEe^2yQ(`3s4RnyLV^MX1%)mrE2#zr1zq<3yd4Sl{T{Xgp!}}~
zCviCt5)#tdw#qLkD42bqB1r1(?d{;;U~zGAVq&7Rv$L_Wv9hwVsHi9{EiE!KGB7aE
z)6>(*$;sBn#>~u20}NJEQxg{#=i=gGWMrhKrY0sPeiweP`lsaI{CBdBj*g0o@;}UX
z&Oa7|gM;rb|5*H|{y+S8@ppbS)W7haYIAe*Kdb)#v%QzU$NP8re<8m!-)a9z_%9j$
zQTX?}@23Ag^8WHYz(0cjsQsJvpYi@nuzzabd;f=det!P%cK%DSf2Q<*)AhT8yuAEB
zQU5dN|C{{Id0#RtEUfo!@J{|`@&W<^|6b7d81ICCGW_occwe}GGW_S>_($s9>wS;i
z-`~Ga;Qy?<e~X)li|dMsy<6xA{0lDh^zUAtR8)?%w7!Id@1{R+as9BdLoqPk-6P@Q
z{~h2E@7s^}ZttDdl+~fI|69=f2e|$VB>y{r{~wtB8{hwh(Eo|F{{?0LiT3{%6aOnV
z{wMzYf8y<bAJzPs!|x+N{wAyK1O<h1@vj@Sc*UpG_k+~VQd-XH_GZrCjUB&2DH(rp
zw730gYH3U*ZRz;+yS<~e6V)eH_T!KAE>KWZP;!!DAh*mjZ+9=#6_@1DEMpIWKfN1G
zeI~ft+EDa<(t)NhVdP{a*r8$srefGlaB$Rp$y78Nh$QY|%}2yB=q#sLTpG<bGB{_E
zXNoG#=0e$^j6!*ZQqW0x^D5hZcwAoU+}66&l>U}lMB9g3#gpe#RhNZw&GU`(EYI%x
z&AdF?*zYgDW@bpY8XF(xE6weNUxG?}uIszquUE{oAFpLD)k_>1+;6)d!am<Lf4;k!
zT)Ru@x+(fiKI|gAbuT>ar2qG6pU}W<`+?2-q4m8aPqRKi<<aWg(EYZ2%k!tlpH_3*
zH3ysN@5E|oAGR>Pu%<C(mkqaGu282xzddu=3mhqQvF*R`+HaXraim^+#t0aJcHmbd
z0qscr`ti`Jo8x&Lm(X?dBsb+fTrHyCXtP$w3}nFDa<Uy5`%=C9NmiZu*Y1OWPWJse
zq0iq!`(Isg--TbK$~79H%hCsP%V0rOsHv?OQ~qf5gs(7KBEG+!Uc@Ile%lGZ(9(P3
z-UoCX2Yf9zDA(Wv@hkxl;sUJpIgQ))ksv}L1ni<?xT*ZSDzyS0?hQI$sK87R1)fRY
zi_c5c&_Rwuy&N#UWAqT1%M`zZO^*|~t&wqqP4!X^NUF|)Y7TR;)Cg4CA1LJ>re(Op
zUbvAZz^kS?5j*at&$Zmv*tkALrLK*GV}%a6nNY^d((c4`$q^<xQ$r;mYdlK&PWSG*
z+}8NGUe)rgfJNL_Y)}g)_2u<8K9{$gXZ%PTW3$gq;raeo)|fhOlmijeRzvb!@dnY9
zUr90y)B&=+p$eCXDvt&R#hd~>BErIgf={GT(zHmh8I@WXoG3cEMA8(;iK$N`7$QMs
zhQtW`H_bBi=7cFhUPTMimfRV6Y@q@YBk)#!3i#<@5#qk}Qy2U!;189&@R7rtop_>y
z!Ij#s?$07j-CZ4R9xsnk)eSnn1h35tH^@xC@OVm7Wsaqsv-sLh+{mbrDLhsG`q8#*
zq|_*~Wz;RFI7TVsDwi)Ua-~T#6wTW3+{<;|!TOIi(gOKW0952;)R%p?8!<noRX1)F
z)IP588E$EHCybi@sj%0Bt?+oeIuzP*mOzwCk;a|13hJ5X2k}9}d>Y|-234Bm@J$Q7
zsBgJCH@~XkA|!H5@g}EuX2FJ-Lzh?+Kbjb6jt}<PWslnBX;rQIMQbEztLT-?lww#X
z=#?#-TUE{>cbsoMhG0@0oplGAy174KkODDNeh>z`y1UmkS4YDFNki*(8EwqJ)PW(7
z7mU6xGj?#SmWgb!u^6A7wLT>Ds2k=c&hGCkTKpb3Oz!*U?%tNdhB1wr`|Jp#D1Nm7
z)hocqXMuqCmkt6mxm{uj*2|3XUQ)&EQ;@&{C`6wcR0E?pd1_JAbM$40k?AzApepWq
z#m2uU87AkVv`OI4D|-_#galflfx@YOow`D!^a2n97eqLbRVrk&%NxdK{qM1Bf1HlU
zY|~MP1fazF(^bvG0;w{%UD<VO?^@RgN`p=q47LTNEmVJCYn5QQCQ69G%hrxgb1XqB
zJLRKRRE&QuH;&m&!9`^8ts_?0?422jv_|)!OngyZq&taa^E&y&jj+!SjKWb%jvi88
ziCzZX=KC!w`)YBI!rjeO2To)ie8xyZLPX5=KfWW4`TF6HJ3ksIA`}GfH*Wu^k2mT4
z7gNoxyD%UL^BaaF$)Q8L{3Fyp9792miPg<Z#*Zs4>7DEp3DH5cty}gAOD0u#-%*>2
z&FyHNZ@^RDUcK=XEe_@t`YtxT<Lup`kv7T_!tUFRVJ^J;wi`_Rry=g4S?(m5ad&cI
z#cot8Y>mhaUfO|z9yh8~&AA_cT+mtly<P=lESdi}98m&zUbw}7C{XW*Y-fR?tX^kH
z+^203lj(wW`e(YBGd}7f+M)*~%62l^^B3o}EEU;9h8!N%5AbG!(1U;4uX{W?;V0!-
zdMY`;1Q|<-HR2;RqD;V-76{$Xn}2?i80Mz9uem{A*tsgJ0`T)Z>FajIJ(yJ5>88pK
z)tWr#%4y-_YkaNJAfugLFzadH$C8LOY`a~25}_+LgbV>o=WjHj3zL_qwpW_0V&zYa
z?&{Iw`h9EEWPSrHNkni^rWTPgQgBgOS|@yGe*C$<_w-ij22KX3F=kI`=?Al@sVuvY
zm$iXQ1~@5Azyg)k!NyNUIXmDkg+>G-nY{sh4<~S_Dj)i}=bKs%5WV0;#hvlArnar)
z)3b44nVmLVQq;;1cHCg>#??XMncwaTFo2$Ls>@d6qTKl8MMltz7&mgr(*E|EEaOlx
zg#?63A`7c;fKc)6%e*k!xP0X|1T+SMopRq!lxWG1qEf<tJP|!}fZNX{ZsNokIg}(G
zi6)ncMMt3x<~-A_QLx1t(f(z@5lKia0Mg}i3mw12Kle99B6)oeDwF0S6`(H?65W5v
zj<7TCsR%a+;O(o%HOjpcf<NKu^X7v#+@2Ug-eTOr9AC0Pdb$QC=WowWT1VSlv0kc@
zzkcF0U~=W6d`|Y&l!bWImA7>4`ly5+VV22VfIv46B?WJr^?WB{$~cOi7W%zc{dkqg
zz@!^WBRnA0{zGT^g5VUz7GynPjuKQ~17gH4y?n4g#8)v&Z&Nq_pugP1YCObeB(aO%
zj)WhN5XoY7kbs;->Av0lf>-8~s{^M}spFP;F?Gdh<|fkXF0~O4<Of0FK<R6SYxN+<
z!e~S@p!TMzfkG|9X8aBYFE)J+mJ#+h6JK+6U^J*Pr=Y=xeV7yh;^gclG{9C?E3niX
zv@eQNirz+*BR1eD^P=Em&C~8M_lNtk8~6oww<JCH3!WO3vBA#1giKphh0;Ld!5OPm
zTq6!NV%S1w(R65ds_as5_CTZo-+gC5CmTds38n=%c&WF{<Vp-~*vU3R^Lf=&%Rwhk
z*F<!&^~%E7h~;cB6>2}?X;a$)36iKOj0v3Fp{LBXamKuKtaa`w7$|%RXd0Cqfs>x)
z%oCI$gf9E;^HhsI*2(z3CT8M;4C3XtYt8nbp9Xb7Qs*A)3ecz1MJ-+@x%j~VyK^WI
zHKos_?kw2r_XDB9-?R6ti9ZM9J-wfVgvJV|{hMC%%8n81SEBd=e}^uhw2<Wn1%->~
zrrq@ei~HF>)>ZHHa6T2)TLo#oIrS8Idpo!~HjK-zgN?UlM-TtrbO6lyaY`RnMlYwn
z({``(f<9(g^VaafhlHOpGRv4k9wxnseib)$<Y_O&pTt8JRDpx@kyCl79}$_SRX&<<
z2=Q-~R@pmybhRtS+zZ`xjze6H9kesgqL=g~@bkIh_1!0Q=6u6lR@vVeHB`4N{5iSx
z#TJWrTBGFw5}7R@8+D&j8-{MIf9#U@H42e-w~vlsO{7!GjK8YMRK2ZEP8C$32v%<`
zFKZc2=dNpSwK#Z#z3M&*ZaRD<3e*srUvf#(;XuBV3fJaBgH6IX+);xUGi0vAQN<!o
z#94@teNKm{XsMI2)Q;Qe4O5fH=;xN5=!Ue3>>AXZp6l@Ta$y2%gXvTFWKTxv0@Bcu
zsa#W;fCx}Yu(g0vP_BlMVHggtgl1g0kQ70!Lky-<;h7s}JQB56(>$_M8MJ8QJ)I(J
zVA={7b|~q3k0+O%EkS}De*RJ`SJ3#}m6g2vSJTsFS`kQ*T`J&P^@eKq#9^X-wYCeu
z0=L`!H$u+J&rgqk=tU90Sx8b@*5GR>fbY><W?43-e$azGB`lbt`16{0`3UjhI59_H
zcm>b)-h#aQ{-jCqy}_W^hsEuKo4(GXebU$k1w8s54$3G!AC-9GRGlbT;L?&MyT3<K
z>ej?@U2DB=Svc7h+9q)IMi2>NI!*{TVd=-&(+I!Np>0}2j03(O-TvJ^<P~C#FH^?o
zSEmpb<eK5>_Jg$8#E(}>saXRK(4-b*?B1Sn3Npqr!~N53PaI5u4e~qDMAen!6|YmT
z@L1K0`@0M4i^5`9ct})iA}dyAX0iW;5QOl$q%reXIkDo}h!!$0qUT=pMeM`+s*7!1
zxSP6-uYAUcSxWWEM}x-}Mz)C!<iuYF(=!rlFXqh(F8rDwULK){s507XR{VbVH!Kd-
z*y8}%;iQMm#A|8PB!cNnL9P8xp}gh^Cdd^-O6_dEH#TQizb9KnA1!;E`Jo`bgQA^A
zTAQb@1q>NdEd@i30}tP^fbJsh&r~8?eRxGZ5P=QWL>5=53%{l~#4IBiu2U$+p^JVG
z#P>5e3cG&x7j9&(Otdd{V(n0!_5Cyzroxe{zPafKbS+5~*%)+!Ku~@9eB2Ie-lKj^
zxgKUgw@srXGNW#L%UNAX^2tk69w;4og8GXateWqg!5&4^)u39d-uec|AFuJ*8cy1r
zphKZ1w+bC@IoXvs%Jm6CrpOWTa>;TXMnYG|*_`QJ@<HN5eeqjCQ6@bLU@$U1dgcqL
z*T+=>-{9<vQ>r(`Q7D!Ov8Xew_*g~g1b&$x%sQiW{mDmTZl>B;!vSYN7Uk4^iv~Iz
zfJeiKI5CRr_c)1gO0zms`n60V20B6+!CxqHGi}vOlmtue!ti-yKP69#4;+O;F+0H)
z(yh@2D`1G39co<Nt-Dn{^EWG`-2u&?pN(7sKHh(&y|N5*hM^vTV$u!E|GXC9YvqQ_
z0?T5U6NNBb>PH~3d^Gprt_<D<KFRcpF%%lsih%e+;1L^;#1NyjjzSdXH<0U_TV2~Q
zYBL=u^=t3zDeZ0jZ5$IEB&CwzyFo0NY(&*a%Q&2EBVvpnE&p1UzI|-r_bIff)KH76
z{s9hgw_Qt(Mzw9lPbqk?s)2g22%8Kvm|W<PN7<L}rF5L>a+Bul#Z1)hgF(qz9L_`f
zm$1zX(nGHKxG}J}4+))ZMO{L955{Umy(HAvNHfwz+tHR+<xLdya%fTR$XkC8bk=q0
zgH^};icKoRV3ovsq`WIH%mDDOu!ZbnTOcFP4f?V7FZIoOt@06mQf|MzUX?@3dddrj
zKd2vmuwaJx9vz&G3}ao_mY~D5AWDuk&<+3e_H7g9fr0=_^==3Dk6|E14@Z}^sz`H@
z(a}*#Q{kyd_ZIV4GQ7|{JyFBUZR4<AR<2bcvIieJcSTV8E5bX(P#2<w;0ac_M<>)|
zxY=3z@c6LEdi{QXTP#Fh2y|6F%Dxpm;BsvSvIOYovVgcjUanEdA_c~?z_@i1*E|A!
zQ$*MWF<gAu=PGE7oJ^S}s&I>M*KRzO)?KqbTo$;`#Ub<@(=c_@wL6f5(LuNhajU|F
zfr25k--NiPGr(<71w7ue>;^RNk1|X*XOi~BDo69}ELy4)GytHR8gj#65|$rYOhES(
zm#9{Iq{4(Wz49w8q}ZHe+qG^yuJ$NzL2EF1%<BZu9}tX!F2oINh040k3NY$G{^&~7
zp6tB;T^O3j0otEP*fh$L$r4|qEOr^@tvmUyud{wkcN?{6uDF}z7!)D0Bmj@{nOdDj
z!2pH5JoxdYQ-sVNHsoE>h1S_+qyK6bqO(LblkD5OQ4OBo9CXRL=o$H24+WqU1YF)t
z*X4rJd#a3KnvE)d0<P|7%$GSe1sZK-%h5M|{qNT6WV9do;*-lh5%Yl#Koc5giZM%^
zJkzv&$m;u4x%pX6J>WdHA|CPmD~*jVqmy~#tkm3+I_nkEM2-UDKdhP1Dt<C8bwo)Q
z7&_&Vo}LXfxz^(MU^n*d`(#7TZxHrIZXhpC{e=Khg^ohK93o~PT(N#>K5)S(H^Y?6
zy(3d}@%0MD(^3;U$J|L@LbD7fX?L8)xV1l`W-t>B6{3Z7@WE;~!3gX^wkjr3l<=lU
zIc|nXOa+Qd$21#hlsX}_l#cV%Fe_H*N7=N<APxXUbyF(TgHw}U@%3VI68iIqdx~1X
z3l0`BIAttJ>^{7lP<5<L0{%ZNa?~i`gS^}qBbe0Y=|HTQtm1R1H8@FFY@T|ustx|#
z3_|`+6pWm^z8+`RojJKraf-e=;4|SoFd`DZ<OzpheQ6fpgqD%LY`*}XKYP`dUKCcx
zNwR&G&lL2WBg}<f7(=lozuc0ST&5>aMeSmNsKgZ6IOZ2|<glTb;wKukQ-EBdPgTW@
zR!$_oYGG~vemRWvM;TPQ1AI|xc|?DubLB{z!QC3t0#7?l&BnS&#l}W$x<J`{!_6g>
zocrm7F)VL4_FHBGP{}0aD{#b=JFY`#Rp-{c^>C(j8B^Ax&74#|H#>_7RtdIpaJKRC
ziXZ)i)Imf^N`9uQG7G0+kV5WUd^2y+9)Jb~m;&fgS_wJZbGC~+^90s;hn~pU<|EG}
z8T1^ckgIa$la2;Ea=!kZ=zdPD^;Nih$~_(h7LCm9oI3yhpoQhkY<W@@#B=eyu}^S^
z3T}X@ooqy@V*+wG_lNQ>+VBc9t0lTm$LcH0IOtG^dxBzpbSHjXcJ<d~$5qBHmRZ`A
zg?yOhO`!1>9fUqfgctl^LM30>1gl7?fBam@I!6yOl=DYfK*^_4{SIrgFfil`BHcj>
zfo-_>n6WH2<ArHHZM(b9{$_cQXmLb~#!|s;*<^Rfa^8~oG`<%h+ISR{sBVYQOy$<z
z<)9RZkWwsYarQ}?U}0?hkexrVeXr<y{A+I8Ppe`Je9`@QT$O1<Ka9*M0s>--jC*Js
zisrB&jmGWrM<aD6oYr(k3dRn6QNhjwF~Pi$mJLH68#us3<q=hyAZcPfnNk|>l$$V`
z_wH<erc}6=Qglp=RN)inG9gG)=Zp`;XToZg6jSA}v7NqA5uW?v!*C+^aL3}uLl<1E
zOCc2MRVr^Q7;+r6t*9U80J+5e^Kuy_p0rW1rctTGn4&Of;t(TOt7TGAR2)CSb;9>Y
z!O|$|%K!lQ*oA&!ZddV9u|)5ny`r*H;ue+HUWSA;lnMzy(SM5?4hP#q8fJ>sX1g!I
zPbbIR1YvGzlYQe#8*|N!E<&0zfMI~)u<S5^Y&iFrYVAQS3aTLSVK5OoTc)BfTu;U+
zaUwyx+d&w;VoK{4ZNc#A(_dPg+&(LJXKd`4FACAsC-~fvc1A^@NH2xcSSSFj+fRs{
zq8Qp`G?5d7_tXz>v7ov}&3=dB>`umn86jI<d<e!tYdmO7`r}8^4Ofg4;<EcCeKvSN
zPHvx=Icx{zWK|K*Db0i(T62k5(UV6URFi;&6pr@p-;}5Hx5p5ap-!XTR0;Z1JRFg3
zMPjwC<mpcRGHc|hV&`j%8?=R|3N3R&4E(m?qTR-zR$t>mMUu%in2M%tCY&fHe_3ju
zu5xIy3vIl#g0$Y%^XKmjkW`y(XXp<??xEBPy8;IaL=lX|$eLvf>C~wdbF=b)@XLN+
z3J!l14HA?wsz%9hYW22SJ(HZ%LS~l5u?{K|D~M#F0ZC70`U0XfzQHnyUx__ty$MJp
zjoS`6ChbFXCHvHCNCL2gJ~T_3AHuDGv*`X{l*v$!3!nomVI{(IPA;Tpj9KC<ql{D^
z<3eFP{VP15iV<t5^1>T~;!Lyn<i?*(?y#;6VPGrRn-Jj>9bHlMhQk+XzcS`H3wMrS
zH}mP~-{%`fD10COVh?;d3Ew~lq<Gx^5~0NVA~}dM9yg4QtH`l3B2f{jA2b!-0>6&t
z>?FS@k7|x5CS~~PQ_K4KGhqOLa3fqgOr3`Um1g^daY}2I`(`yu8Z+S}H}^3a^UIM1
z4Tz}nJ2HT>A)Fjh`xy9jUuv9R-sC3-GJbP8jRHoa31_052a{s6l<C2Q_)X!~;|v3c
zM}^673#!#&cL)JIh^v$R0B5%f8AR?fX1*UrD}-bF3c3nNhoTIiNieO%L6-`xOfa18
zcQi_cFIXG0U1>0(ZodZM@xdg`mpS8V8J6GBlk5Iq9k$Q=4w>`zUE;-J-GY$#J_es$
z5)(!!U_)~AKg(x4Z1fqFlHr%1n868)yTI!*(8b=mCe&q$)e7;ex{D<1t6RAT8nXKk
zI97PAPO%}GoaTOWL(uA1T!R*CgsW>Ad8OU%BhxCdvJ%YAbaygJ;d`lX3~^~Tc_?+V
zHj%e|1A3<lzcRYwZ|%wwX*jBi+2erWGvLr)&MYvV00?uOziT%Pa>aW?3!7H$U3h^W
z{#G2viKM|*f`z(h(3;&PG|A}1!x57Vjfe{nY->q6C-YVX=3HLLtL;K>;RGHoAJtNM
zcCo)vUI0u_8PSC0uuO%KkCR&N@njVZL=m?oQ!RUi4aCUwOmxu0i3LN>q}a9?g3dG5
zUZ6?v6?MLwy1vW7n|l&_neL^D9<r&A%`fKwp+cR(6iSNP3!@5(DBT}nqZ#a_Fy#`1
z<<d4L;qd#O*i6INz@Wi`k!02c{9~4*ME{7gj2ZCNv%e5UN~`iy)~}#%R_W{F<&MG&
zPaz4mDRyF=Vz;jVv)H0I7scTQ+s*2=qF2X5d{CCSRh!8^hkX<I$Qe5{Xrs1)E>Aue
zU+6u#?T2Yb0gHf;aHRfVZ3OIe)!**(jE%%}6Aw|OWN{DLvQR`Z@=Hz1@5&aH7<ApE
zz)ML{QhKgUzR`q-w8%dW_d#7aDG)Y)wt%QZ*m*mH7HjW-Ld3#KAy^QCSi`4M9cO?5
z4CGtMr6Q39X3p0=M<AS}d%~SKg_d3>g`gK%%lkNV2N#9xa6m#|6WVNcMB7>}i|W!y
zSZMwPcXtLyVe^5y&Siv75_Nj9cY^w7B`ub96Q~xuQq->2wI8Rn@<;NdS0>xq=|=n=
z99JK5@uk#C8hr}sArumP>xEQEjdspLDt`*j22Z?Z54kmWSdO=8V~}a7#UsA&7FLzH
z>1ejGxE;}UhzjJ-Zm-sg;%^K|pSMgT7$xq<(YBZC?$wmvz~I|X#ZN%xjgsa3)%z4Y
zOCVh#)Hrrw#n3s_4D%Z5O<avaYppD`CiW9;`J`Mkv%CcK?2`PNM-n~nVY7oa{8<ks
ziR;d7nMiWFm?HPlHz=z|kLXfzI6gGTGcif>mLfYXcSXE&?oNM?G3|=zS#QIWV`%Zq
z6RB*Vi@C+ETW<Jm4-z;bS9K5XSR-~6GZz!62{D+A6^AT~&&xKI!(7fwB*2zUgn^<F
z@+UM42t@eoZl}@vt=g>}S;l-mcw$}UJ=tS@M}ibjvhdbb0Z_6&$zS=NNuTZZ%f0XN
zUM`aog(Vfg@bHq@!ge9!$R{IMXc{H(84`3)aK>sbDc;PIu1pEa)Q`4|+I3AGmJ|um
zptH<R26--W5<yI<nx8_GU9$2XXKa-RO6hIukWXcQu(W&{aTw<s@>MH%Tf99SmoDa_
zT}3C3vXXX_0<&s3A|!I<dCi8JsyMYDLq;Eg4cEzC!YF4$aX;<$u+$ynodb_0k}GrT
z)aQ)c=#Zv^2^8>I4DcwoshU=YtOhjJU@P1Q0zzS|{E&oNH8Ns<Y5!(yl_o9t(>o8x
zKM&VpV=B~URmu5%H}@*9ofVe&7-nIYXG=8XO{ow+6n3TO28IhJ3?)3~V$f#|NfS>h
zMtNOe;C9u_o$*%9VxZ0%d<|m8+lrAqX)egx_lSG%7ak6w5(|x84RN@5XkZcHMS%@C
z`r13{?)yQ=RHgDlSPq^Wl79KJ4T&d;ak!^-uLED;Hkq_fS>;KwtqM^`1c4_PUh}~r
z{s6V)_pi2b*ut5r*&niUAKRM;qT!as_*gJ!`p0bSRNV2ThY9B)A@>^LVuLLt0b@yX
zp&tzY<mv=H!i{j?;!?5N&sI4oFF$9g-12k2NN|%dy8i6IkE^P%v~_z-Q3mSQ(t-Bk
zWur7N!=agJ7s^Z7iD+)9adb+e^t_G}em$+%&6#nbx`hla6*SODDwlG(U;&2~6@LWG
zd<NBjq+U8{y)Y#SAM7gN>>qtNA<umTZY*jAB@96gLXEjIMj<v#x>ef7yVz-4N^`bu
zsbBMylA2?krEB-NH+HiaJO*2#z0{_QAnFJ?t(f{|SdiaWX)K$6VC)olKW#tj4$%Lt
zi}9@e^|5;vM*X4UsjKi1Ta5)KN?@=b^c!{qI7B?-j1z`r3Q)VG!IIB=?fZV`2igKg
znKTO|RF!#D$ubemv@94L3E1a$udBoqVJ^azUc;q*u?C|7QlYR>!<FDB&A2XTbnuai
zI73~qY1M*NXa(~8a_X~U52IUf)J#KdkIRBqtzLSt#kr-4r`%t78)wEVm_6w7T!T87
z_jj)oc~a$Wh>JyUy;ZLEMDjpKsz%yEDB1V0EG!#`4gE=?Id2xUgy@q*O3N7RxRv;h
z`=H^gsjusA*8N>jJM$N44Vs3Yra<&*9JN)DK6||6Y;D!rK3yN6lO&=`y7B<>Q%}t^
z5f8HY_jBR)2}H?RZKQ2@Z3bc9B(75K@awnz<h82deyU?_+ImK+CI*x?)ykWjA>lE}
z{$fr*3G**<7<e@_vPxe4Df7QotYgTg&W^54<MOkDp#I%dwhWGZIR2$?Bk@c6(?G~2
zXUn|!aIKttjdsyqBh@UPLIzaz^4tY{mBD#zU06Rl@T~tY_g!AmH_z^E)W<&_Q3TXQ
z`L@832`czzYHxCKvPq=uP?+Lfaa7O%O8Rm*h#_RLQE5iFk}pDd;Hlh3Ye;%nGFp}J
zP0C~l=dN!aGu}L4uPdEky4096UEEm1<>xOgabc>ndp{+f1<MebVS01Ihb!lRc(ifd
zQL*Mv@1Os;$<?dk^uz#XTE9z4?)-cKL|(3V;dTzRil9@}y@+4MDK^}5;$LSsFx?GL
z^vYd>t*r(>&U~t=<RZuj8UC98wHe&0KK9WJ#wsIlHlTXaCzjS__`)GZTAhu-lyY;n
z-e{se+;nCJ!5mz-#*SY0`LQq)3g67~60kls0)1|6@nr#<a=mGt0nLfGQI_m+a*ZhP
zzSS}arGlm(3X1URU%v%tz@Uwd&gKYc6&YIm)}R`{ZdDr29p~{w<#2NJ#LgIrM1S0P
z2e=`t4A_CSGR;fg$zYh2u|#C!KqXBJj2~REChZViXR*K5Jw_~ZQfb2hl&IsiFbX?$
zq}ztH{o+;4!1zYVblOxY{I$c=Ou|~A@9ooWXS=(x87v-NQ*eJfis_36aV1-VzR8BI
z1XJAkTofR^-T?XG-hEM^xMB<2+$mDh>+UISVZ@m`5<bFDJG4e0dnKKdltMWjTQ0K+
zpm%5wL*~(kPpa?Ojpcf(@bM%NtX_C!d*rDQ3->rg6dT*E@<E7TxyYh+7W;F%b~wmv
zneM?I{3+7zxYMNmYtqOusLiz?9RCbduvGohuuZ<%fK-ZPpEl5iFE;QyX|1v_@z2`6
zzt6C%A&{ezW(SdZN`lZPmaK*l*s8=rOJ~@$VC3E?tgi%BdLSkrnv&o_zZ1B;%A^~O
zN^VqfmHo(Uj;%)RAwFRt+iC?Kr-Ou|G<1Y)=8paOleB#Qkj-@gd5E6$;ny*2B*aqV
z$gm`+4Y%R)<Bez2U{-C#XLo@lqpZcP7FQk;h-Kw^{N3OsSM1xP(9C9yQ!}8V7cIy^
zZ7=IHaIoj4B);{J5m><BzUK2^@ag-*A{YN1xzw!gw}$Lh)(Om%=I4iTZHTVogFEo<
z*7PKq&{oEp>88H8)9@<6)16M@HZPq;9vd9WOV_@LPl_$6{(u9oz)7bDpjIFB;Md4o
zuvH<eSPyY2gI+T;W_#JQO>DEmZ<JDDp;bzTfeJdMo3+aLtxL1uvCJl&;01rfQft6O
zT*lN?L4waFkl2@pJnnZRXt2UlL9JvH2Olkl&B>vQVDgcq?o!!kJDb&lGK7^o-*QkB
zG5${$E+H*(s{Yp!v_ameD7<OF$Y~uUVD03VdPMVE9OAR*X7{nVNWp~zS#69>z6LkL
z8Rn-LtCKU6RV2c10RlA^x_-=*dBhqAUmii5L%ceQ-XFeSY)e#2$^PUzd%`K@7hCx=
z231O@rsBk!FsV<X3Rf0@54EPle0@fH*QSKsnw2kgLZf3CfovR$r*qjdM+Z-0nWV``
z;CkI2WU?~biO&w(RGN)~q_j8Z_<+N`FWR=ar|mz_l;j~<UL)k-3h*%PCTJg@^n%xQ
z$z3zaX{XHdA-#|1Qh*}{*3cU?^E4$clL_pL`;;f3*qjrTXAaC_P?D>g&FZ#H*y@((
zM4khr=~)u3{fV#84R9`HAkUnUKH0Mr84|aUvoXG;-LX*ovjPZQYM_lWeAf89bXOPH
z*gVK7naP9~eD$S4e1(3>MT)de)`rQZaZm(u3UY1KTW8w42Bwa-A6!f(lW^YZ{2cU+
zW~&Pb3)ws(VKcFEQMzNiCR!^8=k}{1doYW6(z#+LSKc~O<F;DcT@yV<4j9ybqi;mf
zBOyKY@{41&So?s;Rj<rf)R<3tAjn(B_AC7ECFe$OTjx6jKx)8V?%K`h(+s_q+&pN2
z&2DsfAu@x(VMIrW@Vyg--rXhx>ryU&XI@SS3{l3n=$&}kV-uClvnHPI@OTwekUkY%
zs2$COQFt^Kw57;ut?%WPHI!fd6@)sEQu@=>*OL=yc>e)YU3Ead&{#Y&(-p(Id2|7{
zjkBG=AjstBL-GZaC<~p<SIP6{3DLaMlPs#P3uVg^ni8oeQCRum@W}0CFAxdwUEvOv
zGv$Id$=t~jkHwc;#o{~!LKM+SGhcG)Pc;1r5p#YGc9}_@l2xNLLxV?CgFSnn^z@Tl
zVq|eCoj@L`$?W4Y0RaoaLk?sB6=%VP7Z)7WZPTbe?6sM}nQC@Nz?tFGYBDRy`t#K<
z<p#jiz$BCjEJw!aeZ;tz%41<p`Y>PM&lvQ(TZ^<K6HFy?+LV#+`>(!d{Oh{C>d|oM
zNa-J<*n3QMYQcd_v|(LG2KZ&z$BPL>rfHvW2j!S`YSsP5yLw|dFz6<Xu&sZTbyH(;
z^6(3Gw=`t3&a~Xcx|<a2)?bg71ZzYK=Q!IqdUzy{-V}CJE_ugaqr|x~UJoSZ?L$C1
zwZxP<@`-Ar%`CS5^~us;IxdIyl$~cE=LY_pY?4=mAoX9S=nrRZZ%VZ;X~IEKne@Y$
z+O&K^LIONI%IJA=L&KgH0V@HK(NQW_z1f4PWIfu`!j|l!{J6&s*qe>XuBO}jV7T)0
zr^hN5I0}~vbUrf?4DKbVBE2%<m|YJJGB3k&oy5~waA^qMi^ro1ni0rQ;Hm}92Xe|;
zq&T;OGm_hf3>je{fR({7Sr3hOqXJPV<*sfrn~=EZ&+1ECSMELdi7zcppFBRN%nUsZ
ze)gIx_+9~%jF0~pP!BM{6|BpYhY{Bb%PW-TAmdaj=eyiBDzZqb_)CW+>{o4oYSWSH
zo$xD`^7B!zput1z*_C!ojU!o7|9~b3r8}kY91b;iqKVQaO%;+9l4(MO1x>mo9_hJ-
zVI1*n878^U&dYuRMaOQ^{d{2G-+UYy0@YTXCEFM_@M;HP*y)&-y+8K842#iA{oao!
z4y}SG5<YwWe>w4Ae_%646{>yUGz>-J;{I|eH-#<uP?RBmRCD9>t!-hV`}&AC`{oyU
zeeAGC6S{+>Ek?{{d2l$Wda^Z_-jjt6!|@!+rH9rkORDSiElV@&W|_QRNZvIe6&fDc
z;8jYO<vgwaXjU$)Gaqg5cS$4SLfrZDq4u(I?62cG0qTgc{g(Ol*33-1wd6;36dP2;
z1eOE-?4IA@7qp2J{F!00YEdD4;fmIsN{qRO^2gMne#_b%V^EE!It~>CyYUrq-rZM(
zDjtW?x)9_tATcq)L8alIv$+DK;pStbha6_e?=Y6rI93#0MIFg(w#)r7`E#T%ipIl+
z7cyd9{r64|nQwn(l;7+1tB}ltnAyHbHo;3t1&#*Mnp(>^krk@TXAUHh4J7#hh-@7#
z_O6@Or9*OQh7;zvh@<D()r;B8g#)tRimRs3?gC|~&P%s{MSLPMxG1+A)BKUMcTz(i
z%5s$<B(ib2FrIX=Q|df;F=$o~C&8+_^q5%tT|TI8HHu@CWOZp1%iK71?T2MeKl_p%
z(J3+}^^g4&IUm;jYkPP9IjB}|1zYz-0lq()hF+I%{?8)#udDf!nd|UGP`|kEyVfcA
zW^6Q))Ot5ZzS`pV_;)|ph@_cIB8PC+LYGmJp0aPC0z1!s<ahiXw?w2uEJUzh-5l?W
zbnM*nyv)cxf#j1HAV;bUr5y4ghA=oSMWAuk3HZt*dlsmbeT={3lSwg$*0`h%ATRcW
z2E;cPN!*qhHs=KtUsS=TUt=tsT0<qBAicG&$7H{r$Tyd*tLb7?qLH7RZUmMIiXw;f
zaNXS5lb%9#e#j;9IOIr&av<bI2UKyQ0hF&>JV^<d0nd#m$zb|WA?%-yWl1Q0e%490
z+R2l|ur00uO3+D@elFCv#Ozn(^gZWGUbw_twI?nzqS5kSyZ8It9bSI3jp!1mrKS_j
z+MV^myHCJhS4PB%0fA9^9nPws5wqT&i%P?;<IeSnTHP*9PM@zJ5vP)b!6q<<YUXWx
z+%BU+_VkcopY3E5U%pA?vp(>)HQTHGHU(TNgT&?#zNJc`pDF>jU|$|*mo4fMoZut|
zU6%IxkgI-R_)GuTe%x|x0^ZS4O}%z?IRBT7WHqUF+0h26h0Qqq!9PE&rozSzM-(sD
zy2AZVk`%6{IF&>aQ}(x+Cd2X~P}7S2f-x4p3XOtY;u{Z&B2u_64iW`kpC`ax)^v`q
z$d^+y@IoqP%7{C2eI#9iy>G%nzeg;7pTR6JQQ#gmVr)Hxs`GpxF@%R4Lfw7mY6ml?
zDBumQjvtH{3G*cCa@UQXR@(GB9uL|~9i*jyb!9I^)d;f2&NZ19<xZ&tHmm!hq=ocP
zC2X*2jOnf@<t^JmM_>RRY~sHE4U8!J{kE>i)sN~o9buef!NX?Q=4L5Is)k-hjJyCN
z;cUB&ZV6OURGMV2u%OqruHxoGRaZZ?RwFd$oSfKCD}{%i5GO0U<~o%`+iAqUEW&mT
zPHHVJ<%Y(8dMvu8#U*xk(9a|BFjPtd^}!$h#K4ZMK&|wTA>tdD=LdOSERC5vIV14o
zAI1SVG)lhnuZ`P<GjIij`)Q3Hlnv+#A>gklqLq1}(@)woNu&MTn6g{?RcnkW=egdv
z`8bxhWHz7`E=GZU&OQ<-FAE{qv#8k~W0oxW%r~BSd0uKJt!Q99nS}f*PF6oNp-9v-
za?J#hoiemFIACKlOHYycHHZ>oRo3d??ND2K;${wcT`c(m<OdauypDbpy+!V)!N3=7
zfd65&qU22D!AgtSx6}89w;qf^E)F(x%H$h2XbOau_YL<XZ>|HEk#0`@!McZo=xr-V
zpL{QFmHtAVcw9$@`%pnI(TJt*B$^Mbz08cSW(`aDHu;5b&VDGb4bKnd+pp@yfu%v@
zrGYTE6RQPc0K+G&^xaaP>6;o_*uBT?<hqr*bL4y5IEj5fikj}h(?!<Nj}vZtEf*E?
z4)zJ&QS&glfUnIq6mO2gf#E8?LM|R5V(>bH%GPn)x{{SNhM)NXM3R4PrY@lQs-5M8
zi^70mL48;O(9wz$6P1?bYegd*)*R&We#blWWEnAAJu#D+yvif2*PNr_USq*+;<sEm
z+Z#laE!5Fe0rDgr6cywdbXIF;k5XD<z>TK5uRPK1E0xa}vLZRvam{_KqA2%fth&W|
zW-&)=6hHBE_-MqWcyI&46Oa+`yUNU*gqXD_l1))rPn6syxjr#ilO@NQU32faa+-X@
zgk2bcsklv36af9+&tJeULpJ5N-f2y<HsDqnsc8pDsvofe(S^4k<Dp5A^+m*rXt04z
zapiL|Qd(v;OBZHMJJ6sa(U?|GAL9Eghvfn71Ce~arlzco27F4kxfGbdaJXEUQZrtD
zd{ac@1X&|rl{ff~J=D<tL?RRJ*>!S|O;NkR;PQ<)8&F?9<hU_qB^@a_z+WB`9SGpm
zjCT$=4nM5jV=Pe>^+A9I6i%ZtM_?MoiI{MKv*KFSTxGO-vAQ6&Vc0IXxhQ-f9$342
zR+SJPc$|>%tVIGh0{7RPb7UEo&2}Jw=<_Rz1t;a$YaR)<q5wAFbunFQ_@fCLP@H?1
zKOoR5oUQ5+F15kRHQ=~2IrQam?{_hBxOLJ*{9ZgF)9_L*fIYH@UCI;=bgCdV1gARg
zDp$Uo;49M8X)|>U4Y;E@j|k~=0`S`MUKAD(APolUm}q<;u`%1Oehjht$nzH&ILW=l
zdSNIZB;UuFE5OFz%C{|_A5usP+Io_cxzWkDi|j*Yun0mD-yVw#ISPHr$abmmT!}>Z
zOAr^pl*DY_9Hi0|Nt)J%1n#+E(=2Gk$-8>c8SF-F@NeKwnJlP=CGtL#na+HwIq}+V
zZ`{^b{q$XYrtaEtA!V1ljs~#{6@=SoaKa9ak!5IEmFBeUpSp1{AgDQbU({4znlw^E
zb)r~F(}4RPsOEc4!M^!eqy^Q<)`vxmMD$dXtfRmvYoRZ`u9Xv^{jOQV=%_$f`xOk&
zXFo~y_%7Z1zi_hI8+uI`l;Ap9@@35y<Wm^f8-_hUG_2n{0`=M7!;?sekoRSRgMF?v
zH}CbY(t9a$g;P>8taBv@HTNFT?W25Sa#!YaIlW-T_XHXiJ3QhL486XvAS}$l*W~{$
zW|cD<Np2ru{&JSZKOn0#fos95q8I;>-Sdm>!Q4m?wu7V!u=7<;H`s&U`Zri;9Zvd#
z8<U@HM2B=om1{{w-Ew(HfOOCfnCHpd&eNQ}#1c%-XvUIKiDVC7%9bHQ9?oC~E9YRD
zlj@jV26cbh4ZZX*1G_54M$|<6>|`6nQ8hkyJv6cN@!4MaJF`n5iattOQc^iFdRZ6)
z2+hZlTh6S;|MPr@5Uplm7<Wr%Inye4c8<0sNpunRmC2D_ryxk;?S{Dh%9{5?ygbk^
zNN2yzf9M<^#dBZaArq=B%du$a=Ze%=q`n`Gs7%L~l=~XcTs~2OA%K5m71o+7=Ca?a
zRJy!C9ngvw&75+~8It_cXnhLJpkp7cf1T(Mb6T3+-sZUQO)8SPqCzjbjr|h`ePmT>
zYfCvyu#fUo1g*MLO0_{(2UQmb7cY(=!X6A?Z6VBbO-?p3F-e-bN?%bLyilAN1*<`N
zEL_s4HS6TW<fQ%_#v#T6)6{c<4295BLe8SRIKysWMh_>dwzgg&R<_pQ4J0u)W*vX3
zJ`<ilFK9$nKSi=)NmERQMSDy_xO{Ler%EcTYM3PiC99)&<+83_Vz9^m&Fh?ggm~bL
ziO0YDe&=mA%7mo=X|c?0wf^H(f~=^fpg8#s>DKJA$1>)89e)W)fYrx*&@d})o6ili
z)977dtU)f#!z(>`w)Q|sm>9lWHYJ#t=EO6r&gf^Z!s7>M^RQ}l6YCtqP}k4Glo6(6
z^@<hb;-%6A>1hLuS|ii9XbUsdi+U`B<K{ZIJ8!TL#r(x0YKLNCNeBjwdH{B_I&Jh0
zt?;?^QO1SAv^4lo&B>eF%F?vf@kr~6Lh9S5x1g(?AAfEGt2<0k`%%tHtG#zgxBfmZ
zb-zYcBMu4C<sKWu%!4>y5|&Y8Y)))k%Ym2vJy8pXdcyog=`d{D`=uYuB@ne??OJ0+
zzP_&fki2Tjp8k6<{jE7@Sy_C)1>z;FS5DNKatoGg6v(m+@<CD=SePpdd%kv7J5u5O
zKuzMK=xDr$zJVD&IwG61?3zMvLy<kVt4VXN2udHrvvicc><&j6V>SnqlyQDF*|+V`
zkM!!b6-W|2-}u3Q+&O}uEpLsdGh4-yZ?jfJ_E99C#vQ1jg<<MnQ60ZR9^e~6#;E<J
zLe1iK&Y=y-d}hUz4bxq7W%03U{LU$Rx|}dzB({u$04@h4qY-$Vw2{t<d@J7R)UoXD
z5~hBuZhVDE;iV#VVC=y;tpVMa@=-rzz%Yrwyr>_b*<N;xv`11j8Or&(<M~ky`R=?l
zx8o|td;QLxlqs{HY}Q+{yyL(DM+$B3P=6ke2ZIA^&JCeev4`#Gp`_lB-u=S6aZKec
zEk?8P%-36A!X&A?Hd<5OD=s|4CvXV)2$QapZ$C%ie)!F^^6!Wo=CCkJwa_&c?CHeG
zoBoQUkawD@vfw2`%=C9Cn*6U}u1WdlBC1MjkIyPK)<hnM6R`T#cc9dco!<JbOKXBT
z6vzDI+u(5J5f=u7eq%Rh*lSvW<rHHxW$EQeU8d&+r)s{Vj2}m%0bBRT6)ji7Q5EC*
z;(GY}1U;!ptz#-|w#K7SU%g{9Eqgg3&o?K^4g#5(PcE~Z()PC0)-`%6aFqCRcwyrm
zjC@~LTH>3o1EJ!mMfGqESN19SJr4@)|I#sI3Iklzz~H|I1J7}_q6?^_yC)dK@+ZTE
z74-dfPGl#OnAtazT?6}h!#1;AvF9rlU>*;hi`k^{w36_8DJ8(~R~?p`^>$K~->vt$
z2`L4!_e=VQr2$ZZUnCo~`xK~&VZf6SsdXPBJQVm>B6IpZj!)V>-G9+$vt<}q76Xd9
z|5oiv@wFcC@B&T~oBhX7LNhM_Les6&B=O{7Udw)g+7*f&F4t-4wx$vckcM9c*dB9A
zYyrcl1Fi8s&#%WLZXd3C(-o4dU+Sv`oDL@XF}v@N)|$%|t+>YtgvAvr^SdRHZ(9me
zk=<#DNtH$M!%BQs#71C$ymf<_*GhjFw745Q{AzVFC$=!3%J%-7m0XY#GZHjH*qI@u
zqCMKz=U-I%*eXLI;(0gG`h8I!A<O&tj5N~gB&{Cvv)7$&f{1|Aor6I)OQk7G29W#d
z(km?Qw-)b~^B-njm+6jI+~}7HT^01QFR?!^nGx=V`{k4MBZ7|TkPOe#re7}l%_-+p
zzWw?9d<*@Zk1xrT|2mL1L#kX$q*&b=ybTAwRWhF*NV>+%?g4M*T(vNH;WmLwL1oE7
zr07d5(t;HNO^A++$8QsiE_~YVWG{`15mxpho(H{%#4CKBcVr3m&x4=aC-s#!y_d#s
z3=)_ycumxw(J`N|%^6jPUYGC5&Xcm_C4F9L-|+Xkz4lZ}$S!)=gfC}fd~Rq}p67A|
g7Ee5gN8S*>m>#o`q3Zko>(8okQp%E5;zs`e0~Vso3;+NC
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b9eddbdd9b831a21f0a0d733d3d4f31614d34439
GIT binary patch
literal 6118
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
G$o~RM%YoAX
new file mode 100644
--- /dev/null
+++ b/babel3/doc/messages.txt
@@ -0,0 +1,310 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+=============================
+Working with Message Catalogs
+=============================
+
+.. contents:: Contents
+   :depth: 3
+.. sectnum::
+
+
+Introduction
+============
+
+The ``gettext`` translation system enables you to mark any strings used in your
+application as subject to localization, by wrapping them in functions such as
+``gettext(str)`` and ``ngettext(singular, plural, num)``. For brevity, the
+``gettext`` function is often aliased to ``_(str)``, so you can write:
+
+.. code-block:: python
+
+    print _("Hello")
+
+instead of just:
+
+.. code-block:: python
+
+    print "Hello"
+
+to make the string "Hello" localizable.
+
+Message catalogs are collections of translations for such localizable messages
+used in an application. They are commonly stored in PO (Portable Object) and MO
+(Machine Object) files, the formats of which are defined by the GNU `gettext`_
+tools and the GNU `translation project`_.
+
+ .. _`gettext`: http://www.gnu.org/software/gettext/
+ .. _`translation project`: http://sourceforge.net/projects/translation
+
+The general procedure for building message catalogs looks something like this:
+
+ * use a tool (such as ``xgettext``) to extract localizable strings from the
+   code base and write them to a POT (PO Template) file.
+ * make a copy of the POT file for a specific locale (for example, "en_US")
+   and start translating the messages
+ * use a tool such as ``msgfmt`` to compile the locale PO file into an binary
+   MO file
+ * later, when code changes make it necessary to update the translations, you
+   regenerate the POT file and merge the changes into the various
+   locale-specific PO files, for example using ``msgmerge``
+
+Python provides the `gettext module`_ as part of the standard library, which
+enables applications to work with appropriately generated MO files.
+
+ .. _`gettext module`: http://docs.python.org/lib/module-gettext.html
+
+As ``gettext`` provides a solid and well supported foundation for translating
+application messages, Babel does not reinvent the wheel, but rather reuses this
+infrastructure, and makes it easier to build message catalogs for Python
+applications.
+
+
+Message Extraction
+==================
+
+Babel provides functionality similar to that of the ``xgettext`` program,
+except that only extraction from Python source files is built-in, while support
+for other file formats can be added using a simple extension mechanism.
+
+Unlike ``xgettext``, which is usually invoked once for every file, the routines
+for message extraction in Babel operate on directories. While the per-file
+approach of ``xgettext`` works nicely with projects using a ``Makefile``,
+Python projects rarely use ``make``, and thus a different mechanism is needed
+for extracting messages from the heterogeneous collection of source files that
+many Python projects are composed of.
+
+When message extraction is based on directories instead of individual files,
+there needs to be a way to configure which files should be treated in which
+manner. For example, while many projects may contain ``.html`` files, some of
+those files may be static HTML files that don't contain localizable message,
+while others may be `Django`_ templates, and still others may contain `Genshi`_
+markup templates. Some projects may even mix HTML files for different templates
+languages (for whatever reason). Therefore the way in which messages are
+extracted from source files can not only depend on the file extension, but
+needs to be controllable in a precise manner.
+
+.. _`Django`: http://www.djangoproject.com/
+.. _`Genshi`: http://genshi.edgewall.org/
+
+Babel accepts a configuration file to specify this mapping of files to
+extraction methods, which is described below.
+
+
+.. _`frontends`:
+
+----------
+Front-Ends
+----------
+
+Babel provides two different front-ends to access its functionality for working
+with message catalogs:
+
+ * A `Command-line interface <cmdline.html>`_, and
+ * `Integration with distutils/setuptools <setup.html>`_
+
+Which one you choose depends on the nature of your project. For most modern
+Python projects, the distutils/setuptools integration is probably more
+convenient.
+
+
+.. _`mapping`:
+
+-------------------------------------------
+Extraction Method Mapping and Configuration
+-------------------------------------------
+
+The mapping of extraction methods to files in Babel is done via a configuration
+file. This file maps extended glob patterns to the names of the extraction
+methods, and can also set various options for each pattern (which options are
+available depends on the specific extraction method).
+
+For example, the following configuration adds extraction of messages from both
+Genshi markup templates and text templates:
+
+.. code-block:: ini
+
+    # Extraction from Python source files
+    
+    [python: **.py]
+    
+    # Extraction from Genshi HTML and text templates
+    
+    [genshi: **/templates/**.html]
+    ignore_tags = script,style
+    include_attrs = alt title summary
+    
+    [genshi: **/templates/**.txt]
+    template_class = genshi.template:TextTemplate
+    encoding = ISO-8819-15
+
+The configuration file syntax is based on the format commonly found in ``.INI``
+files on Windows systems, and as supported by the ``ConfigParser`` module in
+the Python standard library. Section names (the strings enclosed in square
+brackets) specify both the name of the extraction method, and the extended glob
+pattern to specify the files that this extraction method should be used for,
+separated by a colon. The options in the sections are passed to the extraction
+method. Which options are available is specific to the extraction method used.
+
+The extended glob patterns used in this configuration are similar to the glob
+patterns provided by most shells. A single asterisk (``*``) is a wildcard for
+any number of characters (except for the pathname component separator "/"),
+while a question mark (``?``) only matches a single character. In addition,
+two subsequent asterisk characters (``**``) can be used to make the wildcard
+match any directory level, so the pattern ``**.txt`` matches any file with the
+extension ``.txt`` in any directory.
+
+Lines that start with a ``#`` or ``;`` character are ignored and can be used
+for comments. Empty lines are ignored, too.
+
+.. note:: if you're performing message extraction using the command Babel
+          provides for integration into ``setup.py`` scripts, you can also 
+          provide this configuration in a different way, namely as a keyword 
+          argument to the ``setup()`` function. See `Distutils/Setuptools 
+          Integration`_ for more information.
+
+.. _`distutils/setuptools integration`: setup.html
+
+
+Default Extraction Methods
+--------------------------
+
+Babel comes with only two builtin extractors: ``python`` (which extracts 
+messages from Python source files) and ``ignore`` (which extracts nothing).
+
+The ``python`` extractor is by default mapped to the glob pattern ``**.py``,
+meaning it'll be applied to all files with the ``.py`` extension in any 
+directory. If you specify your own mapping configuration, this default mapping
+is discarded, so you need to explicitly add it to your mapping (as shown in the
+example above.)
+
+
+.. _`referencing extraction methods`:
+
+Referencing Extraction Methods
+------------------------------
+
+To be able to use short extraction method names such as “genshi”, you need to 
+have `pkg_resources`_ installed, and the package implementing that extraction
+method needs to have been installed with its meta data (the `egg-info`_).
+
+If this is not possible for some reason, you need to map the short names to 
+fully qualified function names in an extract section in the mapping 
+configuration. For example:
+
+.. code-block:: ini
+
+    # Some custom extraction method
+    
+    [extractors]
+    custom = mypackage.module:extract_custom
+    
+    [custom: **.ctm]
+    some_option = foo
+
+Note that the builtin extraction methods ``python`` and ``ignore`` are available
+by default, even if `pkg_resources`_ is not installed. You should never need to
+explicitly define them in the ``[extractors]`` section.
+
+.. _`egg-info`: http://peak.telecommunity.com/DevCenter/PythonEggs
+.. _`pkg_resources`: http://peak.telecommunity.com/DevCenter/PkgResources
+
+
+--------------------------
+Writing Extraction Methods
+--------------------------
+
+Adding new methods for extracting localizable methods is easy. First, you'll
+need to implement a function that complies with the following interface:
+
+.. code-block:: python
+
+    def extract_xxx(fileobj, keywords, comment_tags, options):
+        """Extract messages from XXX files.
+        
+        :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 comment_tags: a list of translator tags to search for and
+                             include in the results
+        :param options: a dictionary of additional options (optional)
+        :return: an iterator over ``(lineno, funcname, message, comments)``
+                 tuples
+        :rtype: ``iterator``
+        """
+
+.. note:: Any strings in the tuples produced by this function must be either
+          ``unicode`` objects, or ``str`` objects using plain ASCII characters.
+          That means that if sources contain strings using other encodings, it
+          is the job of the extractor implementation to do the decoding to
+          ``unicode`` objects.
+
+Next, you should register that function as an entry point. This requires your
+``setup.py`` script to use `setuptools`_, and your package to be installed with
+the necessary metadata. If that's taken care of, add something like the
+following to your ``setup.py`` script:
+
+.. code-block:: python
+
+    def setup(...
+
+        entry_points = """
+        [babel.extractors]
+        xxx = your.package:extract_xxx
+        """,
+
+That is, add your extraction method to the entry point group
+``babel.extractors``, where the name of the entry point is the name that people
+will use to reference the extraction method, and the value being the module and
+the name of the function (separated by a colon) implementing the actual
+extraction.
+
+.. note:: As shown in `Referencing Extraction Methods`_, declaring an entry
+          point is not  strictly required, as users can still reference the
+          extraction  function directly. But whenever possible, the entry point
+          should be  declared to make configuration more convenient.
+
+.. _`setuptools`: http://peak.telecommunity.com/DevCenter/setuptools
+
+
+-------------------
+Translator Comments
+-------------------
+
+First of all what are comments tags. Comments tags are excerpts of text to
+search for in comments, only comments, right before the `python gettext`_
+calls, as shown on the following example:
+
+ .. _`python gettext`: http://docs.python.org/lib/module-gettext.html
+
+.. code-block:: python
+
+    # NOTE: This is a comment about `Foo Bar`
+    _('Foo Bar')
+
+The comments tag for the above example would be ``NOTE:``, and the translator
+comment for that tag would be ``This is a comment about `Foo Bar```.
+
+The resulting output in the catalog template would be something like::
+
+    #. This is a comment about `Foo Bar`
+    #: main.py:2
+    msgid "Foo Bar"
+    msgstr ""
+
+Now, you might ask, why would I need that?
+
+Consider this simple case; you have a menu item called “manual”. You know what
+it means, but when the translator sees this they will wonder did you mean:
+
+1. a document or help manual, or
+2. a manual process?
+
+This is the simplest case where a translation comment such as
+“The installation manual” helps to clarify the situation and makes a translator
+more productive.
+
+.. note:: Whether translator comments can be extracted depends on the extraction
+          method in use. The Python extractor provided by Babel does implement
+          this feature, but others may not.
new file mode 100644
--- /dev/null
+++ b/babel3/doc/numbers.txt
@@ -0,0 +1,113 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+==========================
+Number Formatting
+==========================
+
+
+.. contents:: Contents
+   :depth: 2
+.. sectnum::
+
+
+Support for locale-specific formatting and parsing of numbers is provided by
+the ``babel.numbers`` module:
+
+.. code-block:: pycon
+
+    >>> from babel.numbers import format_number, format_decimal, format_percent
+
+Examples:
+
+.. code-block:: pycon
+
+    >>> format_decimal(1.2345, locale='en_US')
+    u'1.234'
+    >>> format_decimal(1.2345, locale='sv_SE')
+    u'1,234'
+    >>> format_decimal(12345, locale='de_DE')
+    u'12.345'
+
+
+Pattern Syntax
+==============
+
+While Babel makes it simple to use the appropriate number format for a given
+locale, you can also force it to use custom patterns. As with date/time
+formatting patterns, the patterns Babel supports for number formatting are
+based on the `Locale Data Markup Language specification`_ (LDML).
+
+Examples:
+
+.. code-block:: pycon
+
+    >>> format_decimal(-1.2345, format='#,##0.##;-#', locale='en')
+    u'-1.23'
+    >>> format_decimal(-1.2345, format='#,##0.##;(#)', locale='en')
+    u'(1.23)'
+
+The syntax for custom number format patterns is described in detail in the
+the specification. The following table is just a relatively brief overview.
+
+ .. _`Locale Data Markup Language specification`: http://unicode.org/reports/tr35/#Number_Format_Patterns
+
+  +----------+-----------------------------------------------------------------+
+  | Symbol   | Description                                                     |
+  +==========+=================================================================+
+  | ``0``    | Digit                                                           |
+  +----------+-----------------------------------------------------------------+
+  | ``1-9``  | '1' through '9' indicate rounding.                              |
+  +----------+-----------------------------------------------------------------+
+  | ``@``    | Significant digit                                               |
+  +----------+-----------------------------------------------------------------+
+  | ``#``    | Digit, zero shows as absent                                     |
+  +----------+-----------------------------------------------------------------+
+  | ``.``    | Decimal separator or monetary decimal separator                 |
+  +----------+-----------------------------------------------------------------+
+  | ``-``    | Minus sign                                                      |
+  +----------+-----------------------------------------------------------------+
+  | ``,``    | Grouping separator                                              |
+  +----------+-----------------------------------------------------------------+
+  | ``E``    | Separates mantissa and exponent in scientific notation          |
+  +----------+-----------------------------------------------------------------+
+  | ``+``    | Prefix positive exponents with localized plus sign              |
+  +----------+-----------------------------------------------------------------+
+  | ``;``    | Separates positive and negative subpatterns                     |
+  +----------+-----------------------------------------------------------------+
+  | ``%``    | Multiply by 100 and show as percentage                          |
+  +----------+-----------------------------------------------------------------+
+  | ``‰``    | Multiply by 1000 and show as per mille                          |
+  +----------+-----------------------------------------------------------------+
+  | ``¤``    | Currency sign, replaced by currency symbol. If doubled,         |
+  |          | replaced by international currency symbol. If tripled, uses the |
+  |          | long form of the decimal symbol.                                |
+  +----------+-----------------------------------------------------------------+
+  | ``'``    | Used to quote special characters in a prefix or suffix          |
+  +----------+-----------------------------------------------------------------+
+  | ``*``    | Pad escape, precedes pad character                              |
+  +----------+-----------------------------------------------------------------+
+
+
+Parsing Numbers
+===============
+
+Babel can also parse numeric data in a locale-sensitive manner:
+
+.. code-block:: pycon
+
+    >>> from babel.numbers import parse_decimal, parse_number
+
+Examples:
+
+.. code-block:: pycon
+
+    >>> parse_decimal('1,099.98', locale='en_US')
+    1099.98
+    >>> parse_decimal('1.099,98', locale='de')
+    1099.98
+    >>> parse_decimal('2,109,998', locale='de')
+    Traceback (most recent call last):
+      ...
+    NumberFormatError: '2,109,998' is not a valid decimal number
+
+.. note:: Number parsing is not properly implemented yet
new file mode 100644
--- /dev/null
+++ b/babel3/doc/setup.txt
@@ -0,0 +1,342 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+================================
+Distutils/Setuptools Integration
+================================
+
+Babel provides commands for integration into ``setup.py`` scripts, based on
+either the ``distutils`` package that is part of the Python standard library,
+or the third-party ``setuptools`` package.
+
+These commands are available by default when Babel has been properly installed,
+and ``setup.py`` is using ``setuptools``. For projects that use plain old
+``distutils``, the commands need to be registered explicitly, for example:
+
+.. code-block:: python
+
+    from distutils.core import setup
+    from babel.messages import frontend as babel
+    
+    setup(
+        ...
+        cmdclass = {'compile_catalog': babel.compile_catalog,
+                    'extract_messages': babel.extract_messages,
+                    'init_catalog': babel.init_catalog,
+                    'update_catalog': babel.update_catalog}
+    )
+
+
+.. contents:: Contents
+   :depth: 2
+.. sectnum::
+
+
+compile_catalog
+===============
+
+The ``compile_catalog`` command is similar to the GNU ``msgfmt`` tool, in that
+it takes a message catalog from a PO file and compiles it to a binary MO file.
+
+If the command has been correctly installed or registered, a project's
+``setup.py`` script should allow you to use the command::
+
+    $ ./setup.py compile_catalog --help
+    Global options:
+      --verbose (-v)  run verbosely (default)
+      --quiet (-q)    run quietly (turns verbosity off)
+      --dry-run (-n)  don't actually do anything
+      --help (-h)     show detailed help message
+    
+    Options for 'compile_catalog' command:
+       ...
+
+Running the command will produce a PO template file::
+
+    $ ./setup.py compile_catalog --directory foobar/locale --locale pt_BR
+    running compile_catalog
+    compiling catalog to to foobar/locale/pt_BR/LC_MESSAGES/messages.mo
+
+
+Options
+-------
+
+The ``compile_catalog`` command accepts the following options:
+
+  +-----------------------------+---------------------------------------------+
+  | Option                      | Description                                 |
+  +=============================+=============================================+
+  | ``--domain``                | domain of the PO file (defaults to          |
+  |                             | lower-cased project name)                   |
+  +-----------------------------+---------------------------------------------+
+  | ``--directory`` (``-d``)    | name of the base directory                  |
+  +-----------------------------+---------------------------------------------+
+  | ``--input-file`` (``-i``)   | name of the input file                      |
+  +-----------------------------+---------------------------------------------+
+  | ``--output-file`` (``-o``)  | name of the output file                     |
+  +-----------------------------+---------------------------------------------+
+  | ``--locale`` (``-l``)       | locale for the new localized string         |
+  +-----------------------------+---------------------------------------------+
+  | ``--use-fuzzy`` (``-f``)    | also include "fuzzy" translations           |
+  +-----------------------------+---------------------------------------------+
+  | ``--statistics``            | print statistics about translations         |
+  +-----------------------------+---------------------------------------------+
+
+If ``directory`` is specified, but ``output-file`` is not, the default filename
+of the output file will be::
+
+    <directory>/<locale>/LC_MESSAGES/<domain>.mo
+
+If neither the ``input_file`` nor the ``locale`` option is set, this command
+looks for all catalog files in the base directory that match the given domain,
+and compiles each of them to MO files in the same directory.
+
+These options can either be specified on the command-line, or in the
+``setup.cfg`` file.
+
+
+extract_messages
+================
+
+The ``extract_messages`` command is comparable to the GNU ``xgettext`` program:
+it can extract localizable messages from a variety of difference source files,
+and generate a PO (portable object) template file from the collected messages.
+
+If the command has been correctly installed or registered, a project's
+``setup.py`` script should allow you to use the command::
+
+    $ ./setup.py extract_messages --help
+    Global options:
+      --verbose (-v)  run verbosely (default)
+      --quiet (-q)    run quietly (turns verbosity off)
+      --dry-run (-n)  don't actually do anything
+      --help (-h)     show detailed help message
+    
+    Options for 'extract_messages' command:
+       ...
+
+Running the command will produce a PO template file::
+
+    $ ./setup.py extract_messages --output-file foobar/locale/messages.pot
+    running extract_messages
+    extracting messages from foobar/__init__.py
+    extracting messages from foobar/core.py
+    ...
+    writing PO template file to foobar/locale/messages.pot
+
+
+Method Mapping
+--------------
+
+The mapping of file patterns to extraction methods (and options) can be
+specified using a configuration file that is pointed to using the
+``--mapping-file`` option shown above. Alternatively, you can configure the
+mapping directly in ``setup.py`` using a keyword argument to the ``setup()``
+function:
+
+.. code-block:: python
+
+    setup(...
+        
+        message_extractors = {
+            'foobar': [
+                ('**.py',                'python', None),
+                ('**/templates/**.html', 'genshi', None),
+                ('**/templates/**.txt',  'genshi', {
+                    'template_class': 'genshi.template:TextTemplate'
+                })
+            ],
+        },
+        
+        ...
+    )
+
+
+Options
+-------
+
+The ``extract_messages`` command accepts the following options:
+
+  +-----------------------------+----------------------------------------------+
+  | Option                      | Description                                  |
+  +=============================+==============================================+
+  | ``--charset``               | charset to use in the output file            |
+  +-----------------------------+----------------------------------------------+
+  | ``--keywords`` (``-k``)     | space-separated list of keywords to look for |
+  |                             | in addition to the defaults                  |
+  +-----------------------------+----------------------------------------------+
+  | ``--no-default-keywords``   | do not include the default keywords          |
+  +-----------------------------+----------------------------------------------+
+  | ``--mapping-file`` (``-F``) | path to the mapping configuration file       |
+  +-----------------------------+----------------------------------------------+
+  | ``--no-location``           | do not include location comments with        |
+  |                             | filename and line number                     |
+  +-----------------------------+----------------------------------------------+
+  | ``--omit-header``           | do not include msgid "" entry in header      |
+  +-----------------------------+----------------------------------------------+
+  | ``--output-file`` (``-o``)  | name of the output file                      |
+  +-----------------------------+----------------------------------------------+
+  | ``--width`` (``-w``)        | set output line width (default 76)           |
+  +-----------------------------+----------------------------------------------+
+  | ``--no-wrap``               | do not break long message lines, longer than |
+  |                             | the output line width, into several lines    |
+  +-----------------------------+----------------------------------------------+
+  | ``--input-dirs``            | directories that should be scanned for       |
+  |                             | messages                                     |
+  +-----------------------------+----------------------------------------------+
+  | ``--sort-output``           | generate sorted output (default False)       |
+  +-----------------------------+----------------------------------------------+
+  | ``--sort-by-file``          | sort output by file location (default False) |
+  +-----------------------------+----------------------------------------------+
+  | ``--msgid-bugs-address``    | set email address for message bug reports    |
+  +-----------------------------+----------------------------------------------+
+  | ``--copyright-holder``      | set copyright holder in output               |
+  +-----------------------------+----------------------------------------------+
+  | ``--add-comments (-c)``     | place comment block with TAG (or those       |
+  |                             | preceding keyword lines) in output file.     |
+  |                             | Separate multiple TAGs with commas(,)        |
+  +-----------------------------+----------------------------------------------+
+
+These options can either be specified on the command-line, or in the
+``setup.cfg`` file. In the latter case, the options above become entries of the
+section ``[extract_messages]``, and the option names are changed to use
+underscore characters instead of dashes, for example:
+
+.. code-block:: ini
+
+    [extract_messages]
+    keywords = _, gettext, ngettext
+    mapping_file = babel.cfg
+    width = 80
+
+This would be equivalent to invoking the command from the command-line as
+follows::
+
+    $ setup.py extract_messages -k _ -k gettext -k ngettext -F mapping.cfg -w 80
+
+Any path names are interpreted relative to the location of the ``setup.py``
+file. For boolean options, use "true" or "false" values.
+
+
+init_catalog
+============
+
+The ``init_catalog`` command is basically equivalent to the GNU ``msginit``
+program: it creates a new translation catalog based on a PO template file (POT).
+
+If the command has been correctly installed or registered, a project's
+``setup.py`` script should allow you to use the command::
+
+    $ ./setup.py init_catalog --help
+    Global options:
+      --verbose (-v)  run verbosely (default)
+      --quiet (-q)    run quietly (turns verbosity off)
+      --dry-run (-n)  don't actually do anything
+      --help (-h)     show detailed help message
+
+    Options for 'init_catalog' command:
+      ...
+
+Running the command will produce a PO file::
+
+    $ ./setup.py init_catalog -l fr -i foobar/locales/messages.pot \
+                             -o foobar/locales/fr/messages.po
+    running init_catalog
+    creating catalog 'foobar/locales/fr/messages.po' based on 'foobar/locales/messages.pot'
+
+
+Options
+-------
+
+The ``init_catalog`` command accepts the following options:
+
+  +-----------------------------+---------------------------------------------+
+  | Option                      | Description                                 |
+  +=============================+=============================================+
+  | ``--domain``                | domain of the PO file (defaults to          |
+  |                             | lower-cased project name)                   |
+  +-----------------------------+---------------------------------------------+
+  | ``--input-file`` (``-i``)   | name of the input file                      |
+  +-----------------------------+---------------------------------------------+
+  | ``--output-dir`` (``-d``)   | name of the output directory                |
+  +-----------------------------+---------------------------------------------+
+  | ``--output-file`` (``-o``)  | name of the output file                     |
+  +-----------------------------+---------------------------------------------+
+  | ``--locale``                | locale for the new localized string         |
+  +-----------------------------+---------------------------------------------+
+
+If ``output-dir`` is specified, but ``output-file`` is not, the default filename
+of the output file will be::
+
+    <output_dir>/<locale>/LC_MESSAGES/<domain>.po
+
+These options can either be specified on the command-line, or in the
+``setup.cfg`` file.
+
+
+update_catalog
+==============
+
+The ``update_catalog`` command is basically equivalent to the GNU ``msgmerge``
+program: it updates an existing translations catalog based on a PO template
+file (POT).
+
+If the command has been correctly installed or registered, a project's
+``setup.py`` script should allow you to use the command::
+
+    $ ./setup.py update_catalog --help
+    Global options:
+      --verbose (-v)  run verbosely (default)
+      --quiet (-q)    run quietly (turns verbosity off)
+      --dry-run (-n)  don't actually do anything
+      --help (-h)     show detailed help message
+
+    Options for 'update_catalog' command:
+      ...
+
+Running the command will update a PO file::
+
+    $ ./setup.py update_catalog -l fr -i foobar/locales/messages.pot \
+                                -o foobar/locales/fr/messages.po
+    running update_catalog
+    updating catalog 'foobar/locales/fr/messages.po' based on 'foobar/locales/messages.pot'
+
+
+Options
+-------
+
+The ``update_catalog`` command accepts the following options:
+
+  +-------------------------------------+-------------------------------------+
+  | Option                              | Description                         |
+  +=====================================+=====================================+
+  | ``--domain``                        | domain of the PO file (defaults to  |
+  |                                     | lower-cased project name)           |
+  +-------------------------------------+-------------------------------------+
+  | ``--input-file`` (``-i``)           | name of the input file              |
+  +-------------------------------------+-------------------------------------+
+  | ``--output-dir`` (``-d``)           | name of the output directory        |
+  +-------------------------------------+-------------------------------------+
+  | ``--output-file`` (``-o``)          | name of the output file             |
+  +-------------------------------------+-------------------------------------+
+  | ``--locale``                        | locale for the new localized string |
+  +-------------------------------------+-------------------------------------+
+  | ``--ignore-obsolete``               | do not include obsolete messages in |
+  |                                     | the output                          |
+  +-------------------------------------+-------------------------------------+
+  | ``--no-fuzzy-matching`` (``-N``)    | do not use fuzzy matching           |
+  +-------------------------------------+-------------------------------------+
+  | ``--previous``                      | keep previous msgids of translated  |
+  |                                     | messages                            |
+  +-------------------------------------+-------------------------------------+
+
+If ``output-dir`` is specified, but ``output-file`` is not, the default filename
+of the output file will be::
+
+    <output_dir>/<locale>/LC_MESSAGES/<domain>.po
+
+If neither the ``input_file`` nor the ``locale`` option is set, this command
+looks for all catalog files in the base directory that match the given domain,
+and updates each of them.
+
+These options can either be specified on the command-line, or in the
+``setup.cfg`` file.
new file mode 100644
--- /dev/null
+++ b/babel3/doc/support.txt
@@ -0,0 +1,48 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+=============================
+Support Classes and Functions
+=============================
+
+.. contents:: Contents
+   :depth: 2
+.. sectnum::
+
+
+The ``babel.support`` modules contains a number of classes and functions that
+can help with integrating Babel, and internationalization in general, into your
+application or framework. The code in this module is not used by Babel itself,
+but instead is provided to address common requirements of applications that
+should handle internationalization. 
+
+
+---------------
+Lazy Evaluation
+---------------
+
+One such requirement is lazy evaluation of translations. Many web-based
+applications define some localizable message at the module level, or in general
+at some level where the locale of the remote user is not yet known. For such
+cases, web frameworks generally provide a "lazy" variant of the ``gettext``
+functions, which basically translates the message not when the ``gettext``
+function is invoked, but when the string is accessed in some manner.
+
+
+---------------------------
+Extended Translations Class
+---------------------------
+
+Many web-based applications are composed of a variety of different components
+(possibly using some kind of plugin system), and some of those components may
+provide their own message catalogs that need to be integrated into the larger
+system.
+
+To support this usage pattern, Babel provides a ``Translations`` class that is
+derived from the ``GNUTranslations`` class in the ``gettext`` module. This
+class adds a ``merge()`` method that takes another ``Translations`` instance,
+and merges the content of the latter into the main catalog:
+
+.. code-block:: python
+
+    translations = Translations.load('main')
+    translations.merge(Translations.load('plugin1'))
new file mode 100755
--- /dev/null
+++ b/babel3/scripts/dump_data.py
@@ -0,0 +1,45 @@
+#!/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/.
+
+from optparse import OptionParser
+from pprint import pprint
+import sys
+
+from babel.localedata import load, LocaleDataDict
+
+
+def main():
+    parser = OptionParser(usage='%prog [options] locale [path]')
+    parser.add_option('--noinherit', action='store_false', dest='inherit',
+                      help='do not merge inherited data into locale data')
+    parser.add_option('--resolve', action='store_true', dest='resolve',
+                      help='resolve aliases in locale data')
+    parser.set_defaults(inherit=True, resolve=False)
+    options, args = parser.parse_args()
+    if len(args) not in (1, 2):
+        parser.error('incorrect number of arguments')
+
+    data = load(args[0], merge_inherited=options.inherit)
+    if options.resolve:
+        data = LocaleDataDict(data)
+    if len(args) > 1:
+        for key in args[1].split('.'):
+            data = data[key]
+    if isinstance(data, dict):
+        data = dict(data.items())
+    pprint(data)
+
+
+if __name__ == '__main__':
+    main()
new file mode 100755
--- /dev/null
+++ b/babel3/scripts/dump_global.py
@@ -0,0 +1,33 @@
+#!/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 os
+import pickle
+from pprint import pprint
+import sys
+
+import babel
+
+dirname = os.path.join(os.path.dirname(babel.__file__))
+filename = os.path.join(dirname, 'global.dat')
+fileobj = open(filename, 'rb')
+try:
+    data = pickle.load(fileobj)
+finally:
+    fileobj.close()
+
+if len(sys.argv) > 1:
+    pprint(data.get(sys.argv[1]))
+else:
+    pprint(data)
new file mode 100755
--- /dev/null
+++ b/babel3/scripts/import_cldr.py
@@ -0,0 +1,497 @@
+#!/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 re
+import sys
+try:
+    from xml.etree.ElementTree import parse
+except ImportError:
+    from elementtree.ElementTree import parse
+
+# Make sure we're using Babel source, and not some previously installed version
+sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..'))
+
+from babel import dates, numbers
+from babel.plural import PluralRule
+from babel.localedata import Alias
+from babel.util import set
+
+weekdays = {'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3, 'fri': 4, 'sat': 5,
+            'sun': 6}
+
+try:
+    any
+except NameError:
+    def any(iterable):
+        return filter(None, list(iterable))
+
+
+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()
+
+
+NAME_RE = re.compile(r"^\w+$")
+TYPE_ATTR_RE = re.compile(r"^\w+\[@type='(.*?)'\]$")
+
+NAME_MAP = {
+    'dateFormats': 'date_formats',
+    'dateTimeFormats': 'datetime_formats',
+    'eraAbbr': 'abbreviated',
+    'eraNames': 'wide',
+    'eraNarrow': 'narrow',
+    'timeFormats': 'time_formats'
+}
+
+def _translate_alias(ctxt, path):
+    parts = path.split('/')
+    keys = ctxt[:]
+    for part in parts:
+        if part == '..':
+            keys.pop()
+        else:
+            match = TYPE_ATTR_RE.match(part)
+            if match:
+                keys.append(match.group(1))
+            else:
+                assert NAME_RE.match(part)
+                keys.append(NAME_MAP.get(part, part))
+    return keys
+
+
+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')
+
+    sup = parse(os.path.join(srcdir, 'supplemental', 'supplementalData.xml'))
+
+    # Import global data from the supplemental files
+    global_data = {}
+
+    territory_zones = global_data.setdefault('territory_zones', {})
+    zone_aliases = global_data.setdefault('zone_aliases', {})
+    zone_territories = global_data.setdefault('zone_territories', {})
+    for elem in sup.findall('.//timezoneData/zoneFormatting/zoneItem'):
+        tzid = elem.attrib['type']
+        territory_zones.setdefault(elem.attrib['territory'], []).append(tzid)
+        zone_territories[tzid] = elem.attrib['territory']
+        if 'aliases' in elem.attrib:
+            for alias in elem.attrib['aliases'].split():
+                zone_aliases[alias] = tzid
+
+    # Import Metazone mapping
+    meta_zones = global_data.setdefault('meta_zones', {})
+    tzsup = parse(os.path.join(srcdir, 'supplemental', 'metazoneInfo.xml'))
+    for elem in tzsup.findall('.//timezone'):
+        for child in elem.findall('usesMetazone'):
+            if 'to' not in child.attrib: # FIXME: support old mappings
+                meta_zones[elem.attrib['type']] = child.attrib['mzone']
+
+    outfile = open(os.path.join(destdir, 'global.dat'), 'wb')
+    try:
+        pickle.dump(global_data, outfile, 2)
+    finally:
+        outfile.close()
+
+    # build a territory containment mapping for inheritance
+    regions = {}
+    for elem in sup.findall('.//territoryContainment/group'):
+        regions[elem.attrib['type']] = elem.attrib['contains'].split()
+
+    # Resolve territory containment
+    territory_containment = {}
+    region_items = regions.items()
+    region_items.sort()
+    for group, territory_list in region_items:
+        for territory in territory_list:
+            containers = territory_containment.setdefault(territory, set([]))
+            if group in territory_containment:
+                containers |= territory_containment[group]
+            containers.add(group)
+
+    # prepare the per-locale plural rules definitions
+    plural_rules = {}
+    prsup = parse(os.path.join(srcdir, 'supplemental', 'plurals.xml'))
+    for elem in prsup.findall('.//plurals/pluralRules'):
+        rules = []
+        for rule in elem.findall('pluralRule'):
+            rules.append((rule.attrib['count'], unicode(rule.text)))
+        pr = PluralRule(rules)
+        for locale in elem.attrib['locales'].split():
+            plural_rules[locale] = pr
+
+    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')
+
+    for filename in filenames:
+        stem, ext = os.path.splitext(filename)
+        if ext != '.xml':
+            continue
+
+        print>>sys.stderr, 'Processing input file %r' % filename
+        tree = parse(os.path.join(srcdir, 'main', filename))
+        data = {}
+
+        language = None
+        elem = tree.find('.//identity/language')
+        if elem is not None:
+            language = elem.attrib['type']
+        print>>sys.stderr, '  Language:  %r' % language
+
+        territory = None
+        elem = tree.find('.//identity/territory')
+        if elem is not None:
+            territory = elem.attrib['type']
+        else:
+            territory = '001' # world
+        print>>sys.stderr, '  Territory: %r' % territory
+        regions = territory_containment.get(territory, [])
+        print>>sys.stderr, '  Regions:    %r' % regions
+
+        # plural rules
+        locale_id = '_'.join(filter(None, [
+            language,
+            territory != '001' and territory or None
+        ]))
+        if locale_id in plural_rules:
+            data['plural_form'] = plural_rules[locale_id]
+
+        # <localeDisplayNames>
+
+        territories = data.setdefault('territories', {})
+        for elem in tree.findall('.//territories/territory'):
+            if ('draft' in elem.attrib or 'alt' 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 or 'alt' 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 or 'alt' 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 or 'alt' in elem.attrib) \
+                    and elem.attrib['type'] in scripts:
+                continue
+            scripts[elem.attrib['type']] = _text(elem)
+
+        # <dates>
+
+        week_data = data.setdefault('week_data', {})
+        supelem = sup.find('.//weekData')
+
+        for elem in supelem.findall('minDays'):
+            territories = elem.attrib['territories'].split()
+            if territory in territories or any([r in territories for r in regions]):
+                week_data['min_days'] = int(elem.attrib['count'])
+
+        for elem in supelem.findall('firstDay'):
+            territories = elem.attrib['territories'].split()
+            if territory in territories or any([r in territories for r in regions]):
+                week_data['first_day'] = weekdays[elem.attrib['day']]
+
+        for elem in supelem.findall('weekendStart'):
+            territories = elem.attrib['territories'].split()
+            if territory in territories or any([r in territories for r in regions]):
+                week_data['weekend_start'] = weekdays[elem.attrib['day']]
+
+        for elem in supelem.findall('weekendEnd'):
+            territories = elem.attrib['territories'].split()
+            if territory in territories or any([r in territories for r in regions]):
+                week_data['weekend_end'] = weekdays[elem.attrib['day']]
+
+        zone_formats = data.setdefault('zone_formats', {})
+        for elem in tree.findall('.//timeZoneNames/gmtFormat'):
+            if 'draft' not in elem.attrib and 'alt' not in elem.attrib:
+                zone_formats['gmt'] = unicode(elem.text).replace('{0}', '%s')
+                break
+        for elem in tree.findall('.//timeZoneNames/regionFormat'):
+            if 'draft' not in elem.attrib and 'alt' not in elem.attrib:
+                zone_formats['region'] = unicode(elem.text).replace('{0}', '%s')
+                break
+        for elem in tree.findall('.//timeZoneNames/fallbackFormat'):
+            if 'draft' not in elem.attrib and 'alt' not in elem.attrib:
+                zone_formats['fallback'] = unicode(elem.text) \
+                    .replace('{0}', '%(0)s').replace('{1}', '%(1)s')
+                break
+
+        time_zones = data.setdefault('time_zones', {})
+        for elem in tree.findall('.//timeZoneNames/zone'):
+            info = {}
+            city = elem.findtext('exemplarCity')
+            if city:
+                info['city'] = unicode(city)
+            for child in elem.findall('long/*'):
+                info.setdefault('long', {})[child.tag] = unicode(child.text)
+            for child in elem.findall('short/*'):
+                info.setdefault('short', {})[child.tag] = unicode(child.text)
+            time_zones[elem.attrib['type']] = info
+
+        meta_zones = data.setdefault('meta_zones', {})
+        for elem in tree.findall('.//timeZoneNames/metazone'):
+            info = {}
+            city = elem.findtext('exemplarCity')
+            if city:
+                info['city'] = unicode(city)
+            for child in elem.findall('long/*'):
+                info.setdefault('long', {})[child.tag] = unicode(child.text)
+            for child in elem.findall('short/*'):
+                info.setdefault('short', {})[child.tag] = unicode(child.text)
+            info['common'] = elem.findtext('commonlyUsed') == 'true'
+            meta_zones[elem.attrib['type']] = info
+
+        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'):
+                ctxt_type = ctxt.attrib['type']
+                ctxts = months.setdefault(ctxt_type, {})
+                for width in ctxt.findall('monthWidth'):
+                    width_type = width.attrib['type']
+                    widths = ctxts.setdefault(width_type, {})
+                    for elem in width.getiterator():
+                        if elem.tag == 'month':
+                            if ('draft' in elem.attrib or 'alt' in elem.attrib) \
+                                    and int(elem.attrib['type']) in widths:
+                                continue
+                            widths[int(elem.attrib.get('type'))] = unicode(elem.text)
+                        elif elem.tag == 'alias':
+                            ctxts[width_type] = Alias(
+                                _translate_alias(['months', ctxt_type, width_type],
+                                                 elem.attrib['path'])
+                            )
+
+            days = data.setdefault('days', {})
+            for ctxt in calendar.findall('days/dayContext'):
+                ctxt_type = ctxt.attrib['type']
+                ctxts = days.setdefault(ctxt_type, {})
+                for width in ctxt.findall('dayWidth'):
+                    width_type = width.attrib['type']
+                    widths = ctxts.setdefault(width_type, {})
+                    for elem in width.getiterator():
+                        if elem.tag == 'day':
+                            dtype = weekdays[elem.attrib['type']]
+                            if ('draft' in elem.attrib or 'alt' not in elem.attrib) \
+                                    and dtype in widths:
+                                continue
+                            widths[dtype] = unicode(elem.text)
+                        elif elem.tag == 'alias':
+                            ctxts[width_type] = Alias(
+                                _translate_alias(['days', ctxt_type, width_type],
+                                                 elem.attrib['path'])
+                            )
+
+            quarters = data.setdefault('quarters', {})
+            for ctxt in calendar.findall('quarters/quarterContext'):
+                ctxt_type = ctxt.attrib['type']
+                ctxts = quarters.setdefault(ctxt.attrib['type'], {})
+                for width in ctxt.findall('quarterWidth'):
+                    width_type = width.attrib['type']
+                    widths = ctxts.setdefault(width_type, {})
+                    for elem in width.getiterator():
+                        if elem.tag == 'quarter':
+                            if ('draft' in elem.attrib or 'alt' in elem.attrib) \
+                                    and int(elem.attrib['type']) in widths:
+                                continue
+                            widths[int(elem.attrib['type'])] = unicode(elem.text)
+                        elif elem.tag == 'alias':
+                            ctxts[width_type] = Alias(
+                                _translate_alias(['quarters', ctxt_type, width_type],
+                                                 elem.attrib['path'])
+                            )
+
+            eras = data.setdefault('eras', {})
+            for width in calendar.findall('eras/*'):
+                width_type = NAME_MAP[width.tag]
+                widths = eras.setdefault(width_type, {})
+                for elem in width.getiterator():
+                    if elem.tag == 'era':
+                        if ('draft' in elem.attrib or 'alt' in elem.attrib) \
+                                and int(elem.attrib['type']) in widths:
+                            continue
+                        widths[int(elem.attrib.get('type'))] = unicode(elem.text)
+                    elif elem.tag == 'alias':
+                        eras[width_type] = Alias(
+                            _translate_alias(['eras', width_type],
+                                             elem.attrib['path'])
+                        )
+
+            # AM/PM
+            periods = data.setdefault('periods', {})
+            for elem in calendar.findall('am'):
+                if ('draft' in elem.attrib or 'alt' 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 or 'alt' in elem.attrib) \
+                        and elem.tag in periods:
+                    continue
+                periods[elem.tag] = unicode(elem.text)
+
+            date_formats = data.setdefault('date_formats', {})
+            for format in calendar.findall('dateFormats'):
+                for elem in format.getiterator():
+                    if elem.tag == 'dateFormatLength':
+                        if 'draft' in elem.attrib and \
+                                elem.attrib.get('type') in date_formats:
+                            continue
+                        try:
+                            date_formats[elem.attrib.get('type')] = \
+                                dates.parse_pattern(unicode(elem.findtext('dateFormat/pattern')))
+                        except ValueError, e:
+                            print>>sys.stderr, 'ERROR: %s' % e
+                    elif elem.tag == 'alias':
+                        date_formats = Alias(_translate_alias(
+                            ['date_formats'], elem.attrib['path'])
+                        )
+
+            time_formats = data.setdefault('time_formats', {})
+            for format in calendar.findall('timeFormats'):
+                for elem in format.getiterator():
+                    if elem.tag == 'timeFormatLength':
+                        if ('draft' in elem.attrib or 'alt' in elem.attrib) \
+                                and elem.attrib.get('type') in time_formats:
+                            continue
+                        try:
+                            time_formats[elem.attrib.get('type')] = \
+                                dates.parse_pattern(unicode(elem.findtext('timeFormat/pattern')))
+                        except ValueError, e:
+                            print>>sys.stderr, 'ERROR: %s' % e
+                    elif elem.tag == 'alias':
+                        time_formats = Alias(_translate_alias(
+                            ['time_formats'], elem.attrib['path'])
+                        )
+
+            datetime_formats = data.setdefault('datetime_formats', {})
+            for format in calendar.findall('dateTimeFormats'):
+                for elem in format.getiterator():
+                    if elem.tag == 'dateTimeFormatLength':
+                        if ('draft' in elem.attrib or 'alt' in elem.attrib) \
+                                and elem.attrib.get('type') in datetime_formats:
+                            continue
+                        try:
+                            datetime_formats[elem.attrib.get('type')] = \
+                                unicode(elem.findtext('dateTimeFormat/pattern'))
+                        except ValueError, e:
+                            print>>sys.stderr, 'ERROR: %s' % e
+                    elif elem.tag == 'alias':
+                        datetime_formats = Alias(_translate_alias(
+                            ['datetime_formats'], elem.attrib['path'])
+                        )
+
+        # <numbers>
+
+        number_symbols = data.setdefault('number_symbols', {})
+        for elem in tree.findall('.//numbers/symbols/*'):
+            if ('draft' in elem.attrib or 'alt' in elem.attrib):
+                continue
+            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 or 'alt' in elem.attrib) \
+                    and elem.attrib.get('type') in decimal_formats:
+                continue
+            pattern = unicode(elem.findtext('decimalFormat/pattern'))
+            decimal_formats[elem.attrib.get('type')] = numbers.parse_pattern(pattern)
+
+        scientific_formats = data.setdefault('scientific_formats', {})
+        for elem in tree.findall('.//scientificFormats/scientificFormatLength'):
+            if ('draft' in elem.attrib or 'alt' in elem.attrib) \
+                    and elem.attrib.get('type') in scientific_formats:
+                continue
+            pattern = unicode(elem.findtext('scientificFormat/pattern'))
+            scientific_formats[elem.attrib.get('type')] = numbers.parse_pattern(pattern)
+
+        currency_formats = data.setdefault('currency_formats', {})
+        for elem in tree.findall('.//currencyFormats/currencyFormatLength'):
+            if ('draft' in elem.attrib or 'alt' in elem.attrib) \
+                    and elem.attrib.get('type') in currency_formats:
+                continue
+            pattern = unicode(elem.findtext('currencyFormat/pattern'))
+            currency_formats[elem.attrib.get('type')] = numbers.parse_pattern(pattern)
+
+        percent_formats = data.setdefault('percent_formats', {})
+        for elem in tree.findall('.//percentFormats/percentFormatLength'):
+            if ('draft' in elem.attrib or 'alt' in elem.attrib) \
+                    and elem.attrib.get('type') in percent_formats:
+                continue
+            pattern = unicode(elem.findtext('percentFormat/pattern'))
+            percent_formats[elem.attrib.get('type')] = numbers.parse_pattern(pattern)
+
+        currency_names = data.setdefault('currency_names', {})
+        currency_symbols = data.setdefault('currency_symbols', {})
+        for elem in tree.findall('.//currencies/currency'):
+            code = elem.attrib['type']
+            # TODO: support plural rules for currency name selection
+            for name in elem.findall('displayName'):
+                if ('draft' in name.attrib or 'count' in name.attrib) \
+                        and code in currency_names:
+                    continue
+                currency_names[code] = unicode(name.text)
+            # TODO: support choice patterns for currency symbol selection
+            symbol = elem.find('symbol')
+            if symbol is not None and 'draft' not in symbol.attrib \
+                    and 'choice' not in symbol.attrib:
+                currency_symbols[code] = unicode(symbol.text)
+
+        # <units>
+
+        unit_patterns = data.setdefault('unit_patterns', {})
+        for elem in tree.findall('.//units/unit'):
+            unit_type = elem.attrib['type']
+            unit_pattern = unit_patterns.setdefault(unit_type, {})
+            for pattern in elem.findall('unitPattern'):
+                unit_patterns[unit_type][pattern.attrib['count']] = \
+                        unicode(pattern.text)
+
+        outfile = open(os.path.join(destdir, 'localedata', stem + '.dat'), 'wb')
+        try:
+            pickle.dump(data, outfile, 2)
+        finally:
+            outfile.close()
+
+
+if __name__ == '__main__':
+    main()
new file mode 100644
--- /dev/null
+++ b/babel3/setup.cfg
@@ -0,0 +1,3 @@
+[egg_info]
+tag_build = dev
+tag_svn_revision = true
new file mode 100755
--- /dev/null
+++ b/babel3/setup.py
@@ -0,0 +1,83 @@
+#!/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/.
+
+from distutils.cmd import Command
+import doctest
+from glob import glob
+import os
+try:
+    from setuptools import setup
+except ImportError:
+    from distutils.core import setup
+import sys
+
+sys.path.append(os.path.join('doc', 'common'))
+try:
+    from doctools import build_doc, test_doc
+except ImportError:
+    build_doc = test_doc = None
+
+
+setup(
+    name = 'Babel',
+    version = '1.0',
+    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 = ['babel', 'babel.messages'],
+    package_data = {'babel': ['global.dat', 'localedata/*.dat']},
+    test_suite = 'babel.tests.suite',
+    tests_require = ['pytz'],
+
+    entry_points = """
+    [console_scripts]
+    pybabel = babel.messages.frontend:main
+    
+    [distutils.commands]
+    compile_catalog = babel.messages.frontend:compile_catalog
+    extract_messages = babel.messages.frontend:extract_messages
+    init_catalog = babel.messages.frontend:init_catalog
+    update_catalog = babel.messages.frontend:update_catalog
+    
+    [distutils.setup_keywords]
+    message_extractors = babel.messages.frontend:check_message_extractors
+    
+    [babel.checkers]
+    num_plurals = babel.messages.checkers:num_plurals
+    python_format = babel.messages.checkers:python_format
+    
+    [babel.extractors]
+    ignore = babel.messages.extract:extract_nothing
+    python = babel.messages.extract:extract_python
+    javascript = babel.messages.extract:extract_javascript
+    """,
+
+    cmdclass = {'build_doc': build_doc, 'test_doc': test_doc}
+)
Copyright (C) 2012-2017 Edgewall Software