changeset 381:6a0e7205790f stable

Ported [407:415/trunk] back to 0.9.x branch.
author cmlenz
date Tue, 08 Jul 2008 21:01:28 +0000
parents 4b63ce84c6ae
children 6ab5550b7945
files 0.9.x/ChangeLog 0.9.x/babel/core.py 0.9.x/babel/dates.py 0.9.x/babel/localedata.py 0.9.x/babel/messages/catalog.py 0.9.x/babel/messages/frontend.py 0.9.x/babel/messages/plurals.py 0.9.x/babel/messages/pofile.py 0.9.x/babel/messages/tests/__init__.py 0.9.x/babel/messages/tests/checkers.py 0.9.x/babel/messages/tests/frontend.py 0.9.x/babel/messages/tests/plurals.py 0.9.x/babel/messages/tests/pofile.py 0.9.x/babel/numbers.py 0.9.x/babel/tests/dates.py 0.9.x/babel/tests/localedata.py 0.9.x/scripts/dump_data.py 0.9.x/scripts/import_cldr.py
diffstat 18 files changed, 1073 insertions(+), 141 deletions(-) [+]
line wrap: on
line diff
--- a/0.9.x/ChangeLog
+++ b/0.9.x/ChangeLog
@@ -11,9 +11,14 @@
    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.
- * A JavaScript message extractor was added.
- * Updated to CLDR 1.5.1.
+ * 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
--- a/0.9.x/babel/core.py
+++ b/0.9.x/babel/core.py
@@ -223,7 +223,7 @@
 
     def _data(self):
         if self.__data is None:
-            self.__data = localedata.load(str(self))
+            self.__data = localedata.LocaleDataDict(localedata.load(str(self)))
         return self.__data
     _data = property(_data)
 
@@ -326,7 +326,7 @@
         Mapping of script codes to translated script names.
         
         >>> Locale('de', 'DE').variants['1901']
-        u'alte deutsche Rechtschreibung'
+        u'Alte deutsche Rechtschreibung'
         
         :type: `dict`
         """)
@@ -481,7 +481,7 @@
         >>> 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\u2019s'
+        u"St. John's"
         
         :type: `dict`
         """)
--- a/0.9.x/babel/dates.py
+++ b/0.9.x/babel/dates.py
@@ -185,11 +185,11 @@
     >>> get_timezone_gmt(dt, 'short', locale='en')
     u'-0800'
     
-    The long format depends on the locale, for example in France a different
-    string is used for GMT:
+    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'HMG-08:00'
+    u'UTC-08:00'
     
     :param datetime: the ``datetime`` object; if `None`, the current date and
                      time in UTC is used
@@ -488,7 +488,7 @@
     if datetime is None:
         datetime = datetime_.utcnow()
     elif isinstance(datetime, (int, long)):
-        datetime = datetime.utcfromtimestamp(datetime)
+        datetime = datetime_.utcfromtimestamp(datetime)
     elif isinstance(datetime, time):
         datetime = datetime_.combine(date.today(), datetime)
     if datetime.tzinfo is None:
--- a/0.9.x/babel/localedata.py
+++ b/0.9.x/babel/localedata.py
@@ -23,14 +23,16 @@
     import threading
 except ImportError:
     import dummy_threading as threading
+from UserDict import DictMixin
 
-__all__ = ['exists', 'load']
+__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.
     
@@ -42,6 +44,7 @@
         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.
@@ -54,7 +57,8 @@
         os.path.splitext(filename) for filename in os.listdir(_dirname)
     ] if extension == '.dat' and stem != 'root']
 
-def load(name):
+
+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
@@ -74,6 +78,8 @@
     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
@@ -84,7 +90,7 @@
         data = _cache.get(name)
         if not data:
             # Load inherited data
-            if name == 'root':
+            if name == 'root' or not merge_inherited:
                 data = {}
             else:
                 parts = name.split('_')
@@ -96,7 +102,7 @@
             filename = os.path.join(_dirname, '%s.dat' % name)
             fileobj = open(filename, 'rb')
             try:
-                if name != 'root':
+                if name != 'root' and merge_inherited:
                     merge(data, pickle.load(fileobj))
                 else:
                     data = pickle.load(fileobj)
@@ -107,17 +113,92 @@
     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'})
+    >>> d
+    {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, value in dict2.items():
-        if value is not None:
-            if type(value) is dict:
-                dict1[key] = dict1.get(key, {}).copy()
-                merge(dict1[key], value)
+    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:
-                dict1[key] = value
+                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)
+        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 = self
+        self.base = base
+
+    def __getitem__(self, key):
+        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 isinstance(val, dict): # Return a nested alias-resolving dict
+            val = LocaleDataDict(val, base=self.base)
+        return val
+
+    def copy(self):
+        return LocaleDataDict(dict.copy(self), base=self.base)
--- a/0.9.x/babel/messages/catalog.py
+++ b/0.9.x/babel/messages/catalog.py
@@ -28,7 +28,7 @@
 from babel import __version__ as VERSION
 from babel.core import Locale
 from babel.dates import format_datetime
-from babel.messages.plurals import PLURALS
+from babel.messages.plurals import get_plural
 from babel.util import odict, distinct, LOCALTZ, UTC, FixedOffsetTimezone
 
 __all__ = ['Message', 'Catalog', 'TranslationError']
@@ -409,13 +409,10 @@
     """)
 
     def num_plurals(self):
-        if not self._num_plurals:
+        if self._num_plurals is None:
             num = 2
             if self.locale:
-                if str(self.locale) in PLURALS:
-                    num = PLURALS[str(self.locale)][0]
-                elif self.locale.language in PLURALS:
-                    num = PLURALS[self.locale.language][0]
+                num = get_plural(self.locale)[0]
             self._num_plurals = num
         return self._num_plurals
     num_plurals = property(num_plurals, doc="""\
@@ -430,13 +427,10 @@
     """)
 
     def plural_expr(self):
-        if not self._plural_expr:
+        if self._plural_expr is None:
             expr = '(n != 1)'
             if self.locale:
-                if str(self.locale) in PLURALS:
-                    expr = PLURALS[str(self.locale)][1]
-                elif self.locale.language in PLURALS:
-                    expr = PLURALS[self.locale.language][1]
+                expr = get_plural(self.locale)[1]
             self._plural_expr = expr
         return self._plural_expr
     plural_expr = property(plural_expr, doc="""\
--- a/0.9.x/babel/messages/frontend.py
+++ b/0.9.x/babel/messages/frontend.py
@@ -440,7 +440,9 @@
 
         infile = open(self.input_file, 'r')
         try:
-            catalog = read_po(infile)
+            # 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()
 
@@ -956,7 +958,9 @@
 
         infile = open(options.input_file, 'r')
         try:
-            catalog = read_po(infile)
+            # 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()
 
--- a/0.9.x/babel/messages/plurals.py
+++ b/0.9.x/babel/messages/plurals.py
@@ -13,6 +13,14 @@
 
 """Plural form definitions."""
 
+
+from operator import itemgetter
+from babel.core import default_locale, Locale
+
+
+LC_CTYPE = default_locale('LC_CTYPE')
+
+
 PLURALS = {
     # Afar
     # 'aa': (),
@@ -191,3 +199,58 @@
     '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)
--- a/0.9.x/babel/messages/pofile.py
+++ b/0.9.x/babel/messages/pofile.py
@@ -145,8 +145,14 @@
             msgid = tuple([denormalize(m) for m in messages])
         else:
             msgid = denormalize(messages[0])
-        if len(translations) > 1:
-            string = tuple([denormalize(t[1]) for t in translations])
+        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])
         message = Message(msgid, string, list(locations), set(flags),
@@ -384,9 +390,14 @@
             _write('%smsgid_plural %s\n' % (
                 prefix, _normalize(message.id[1], prefix)
             ))
-            for i, string in enumerate(message.string):
+
+            for idx in range(catalog.num_plurals):
+                try:
+                    string = message.string[idx]
+                except IndexError:
+                    string = ''
                 _write('%smsgstr[%d] %s\n' % (
-                    prefix, i, _normalize(message.string[i], prefix)
+                    prefix, idx, _normalize(string, prefix)
                 ))
         else:
             _write('%smsgid %s\n' % (prefix, _normalize(message.id, prefix)))
--- a/0.9.x/babel/messages/tests/__init__.py
+++ b/0.9.x/babel/messages/tests/__init__.py
@@ -14,13 +14,15 @@
 import unittest
 
 def suite():
-    from babel.messages.tests import catalog, extract, frontend, mofile, pofile
+    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(pofile.suite())
+    suite.addTest(checkers.suite())
     return suite
 
 if __name__ == '__main__':
new file mode 100644
--- /dev/null
+++ b/0.9.x/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')
--- a/0.9.x/babel/messages/tests/frontend.py
+++ b/0.9.x/babel/messages/tests/frontend.py
@@ -13,7 +13,7 @@
 
 from datetime import datetime
 from distutils.dist import Distribution
-from distutils.errors import DistutilsOptionError, DistutilsSetupError
+from distutils.errors import DistutilsOptionError
 from distutils.log import _global_log
 import doctest
 import os
@@ -280,9 +280,10 @@
         self.cmd.initialize_options()
 
     def tearDown(self):
-        locale_dir = os.path.join(self.datadir, 'project', 'i18n', 'en_US')
-        if os.path.isdir(locale_dir):
-            shutil.rmtree(locale_dir)
+        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)
 
@@ -346,32 +347,7 @@
                                tzinfo=LOCALTZ, locale='en')},
        open(po_file, 'U').read())
 
-
-class InitCatalogNonFuzzyTestCase(unittest.TestCase):
-    # FIXME: what is this test case about?
-
-    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):
-        locale_dir = os.path.join(self.datadir, 'project', 'i18n', 'en_US')
-        if os.path.isdir(locale_dir):
-            shutil.rmtree(locale_dir)
-
-        os.chdir(self.olddir)
-
-    def test_with_output_dir(self):
+    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'
@@ -421,6 +397,107 @@
                                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):
 
@@ -438,6 +515,10 @@
         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:
@@ -631,6 +712,109 @@
        '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',
@@ -683,7 +867,6 @@
     suite.addTest(unittest.makeSuite(CompileCatalogTestCase))
     suite.addTest(unittest.makeSuite(ExtractMessagesTestCase))
     suite.addTest(unittest.makeSuite(InitCatalogTestCase))
-    suite.addTest(unittest.makeSuite(InitCatalogNonFuzzyTestCase))
     suite.addTest(unittest.makeSuite(CommandLineInterfaceTestCase))
     return suite
 
new file mode 100644
--- /dev/null
+++ b/0.9.x/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')
--- a/0.9.x/babel/messages/tests/pofile.py
+++ b/0.9.x/babel/messages/tests/pofile.py
@@ -143,6 +143,30 @@
         self.assertEqual(1, len(catalog))
         self.assertEqual(0, len(catalog.obsolete))
 
+    def test_singlular_plural_form(self):
+        buf = StringIO(r'''msgid "foo"
+msgid_plural "foo"
+msgstr[0] "Voh"
+msgstr[1] "Vohs"''') # This is a bad po, ja_JP only uses msgstr[0]
+        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_more_than_two_plural_forms(self):
+        buf = StringIO(r'''msgid "foo"
+msgid_plural "foo"
+msgstr[0] "Voh"
+msgstr[1] "Vohs"''') # last translation form is missing
+#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('', message.string[2])
+
 
 class WritePoTestCase(unittest.TestCase):
 
--- a/0.9.x/babel/numbers.py
+++ b/0.9.x/babel/numbers.py
@@ -165,9 +165,9 @@
     >>> format_currency(1099.98, 'USD', locale='en_US')
     u'$1,099.98'
     >>> format_currency(1099.98, 'USD', locale='es_CO')
-    u'US$ 1.099,98'
+    u'US$\\xa01.099,98'
     >>> format_currency(1099.98, 'EUR', locale='de_DE')
-    u'1.099,98 \\u20ac'
+    u'1.099,98\\xa0\\u20ac'
     
     The pattern can also be specified explicitly:
     
--- a/0.9.x/babel/tests/dates.py
+++ b/0.9.x/babel/tests/dates.py
@@ -29,6 +29,11 @@
         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')
@@ -187,7 +192,7 @@
         tz = timezone('Europe/Paris')
         t = time(15, 30, tzinfo=tz)
         fmt = dates.DateTimeFormat(t, locale='fr_FR')
-        self.assertEqual(u'Heure de l’Europe centrale', fmt['vvvv'])
+        self.assertEqual(u'heure d’Europe centrale', fmt['vvvv'])
 
     def test_hour_formatting(self):
         l = 'en_US'
--- a/0.9.x/babel/tests/localedata.py
+++ b/0.9.x/babel/tests/localedata.py
@@ -16,9 +16,57 @@
 
 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__':
--- a/0.9.x/scripts/dump_data.py
+++ b/0.9.x/scripts/dump_data.py
@@ -12,12 +12,32 @@
 # 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
+from babel.localedata import load, LocaleDataDict
 
-if len(sys.argv) > 2:
-    pprint(load(sys.argv[1]).get(sys.argv[2]))
-else:
-    pprint(load(sys.argv[1]))
+
+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]
+    pprint(dict(data.items()))
+
+
+if __name__ == '__main__':
+    main()
--- a/0.9.x/scripts/import_cldr.py
+++ b/0.9.x/scripts/import_cldr.py
@@ -16,6 +16,7 @@
 from optparse import OptionParser
 import os
 import pickle
+import re
 import sys
 try:
     from xml.etree.ElementTree import parse
@@ -26,6 +27,7 @@
 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..'))
 
 from babel import dates, numbers
+from babel.localedata import Alias
 
 weekdays = {'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3, 'fri': 4, 'sat': 5,
             'sun': 6}
@@ -36,6 +38,7 @@
     def any(iterable):
         return filter(None, list(iterable))
 
+
 def _text(elem):
     buf = [elem.text or '']
     for child in elem:
@@ -43,6 +46,35 @@
     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()
@@ -109,6 +141,8 @@
         stem, ext = os.path.splitext(filename)
         if ext != '.xml':
             continue
+        #if stem != 'root':
+        #    break
 
         tree = parse(os.path.join(srcdir, 'main', filename))
         data = {}
@@ -133,25 +167,29 @@
 
         territories = data.setdefault('territories', {})
         for elem in tree.findall('//territories/territory'):
-            if 'draft' in elem.attrib and elem.attrib['type'] in territories:
+            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 and elem.attrib['type'] in languages:
+            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 and elem.attrib['type'] in variants:
+            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 and elem.attrib['type'] in scripts:
+            if ('draft' in elem.attrib or 'alt' in elem.attrib) \
+                    and elem.attrib['type'] in scripts:
                 continue
             scripts[elem.attrib['type']] = _text(elem)
 
@@ -182,15 +220,15 @@
 
         zone_formats = data.setdefault('zone_formats', {})
         for elem in tree.findall('//timeZoneNames/gmtFormat'):
-            if 'draft' not in elem.attrib:
+            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:
+            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:
+            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
@@ -227,88 +265,141 @@
 
             months = data.setdefault('months', {})
             for ctxt in calendar.findall('months/monthContext'):
-                ctxts = months.setdefault(ctxt.attrib['type'], {})
+                ctxt_type = ctxt.attrib['type']
+                ctxts = months.setdefault(ctxt_type, {})
                 for width in ctxt.findall('monthWidth'):
-                    widths = ctxts.setdefault(width.attrib['type'], {})
-                    for elem in width.findall('month'):
-                        if 'draft' in elem.attrib and int(elem.attrib['type']) in widths:
-                            continue
-                        widths[int(elem.attrib.get('type'))] = unicode(elem.text)
+                    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'):
-                ctxts = days.setdefault(ctxt.attrib['type'], {})
+                ctxt_type = ctxt.attrib['type']
+                ctxts = days.setdefault(ctxt_type, {})
                 for width in ctxt.findall('dayWidth'):
-                    widths = ctxts.setdefault(width.attrib['type'], {})
-                    for elem in width.findall('day'):
-                        dtype = weekdays[elem.attrib['type']]
-                        if 'draft' in elem.attrib and dtype in widths:
-                            continue
-                        widths[dtype] = unicode(elem.text)
+                    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'):
-                    widths = ctxts.setdefault(width.attrib['type'], {})
-                    for elem in width.findall('quarter'):
-                        if 'draft' in elem.attrib and int(elem.attrib['type']) in widths:
-                            continue
-                        widths[int(elem.attrib.get('type'))] = unicode(elem.text)
+                    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/*'):
-                ewidth = {
-                    'eraAbbr': 'abbreviated',
-                    'eraNames': 'wide',
-                    'eraNarrow': 'narrow',
-                }[width.tag]
-                widths = eras.setdefault(ewidth, {})
-                for elem in width.findall('era'):
-                    if 'draft' in elem.attrib and int(elem.attrib['type']) in widths:
-                        continue
-                    widths[int(elem.attrib.get('type'))] = unicode(elem.text)
+                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 and elem.tag in periods:
+                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 and elem.tag in periods:
+                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 elem in calendar.findall('dateFormats/dateFormatLength'):
-                if 'draft' in elem.attrib and elem.attrib.get('type') in date_formats:
-                    continue
-                try:
-                    date_formats[elem.attrib.get('type')] = \
-                        dates.parse_pattern(unicode(elem.findtext('dateFormat/pattern')))
-                except ValueError, e:
-                    print>>sys.stderr, 'ERROR: %s' % e
+            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 elem in calendar.findall('timeFormats/timeFormatLength'):
-                if 'draft' in elem.attrib and elem.attrib.get('type') in time_formats:
-                    continue
-                try:
-                    time_formats[elem.attrib.get('type')] = \
-                        dates.parse_pattern(unicode(elem.findtext('timeFormat/pattern')))
-                except ValueError, e:
-                    print>>sys.stderr, 'ERROR: %s' % e
+            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 elem in calendar.findall('dateTimeFormats/dateTimeFormatLength'):
-                if 'draft' 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
+            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>
 
@@ -318,28 +409,32 @@
 
         decimal_formats = data.setdefault('decimal_formats', {})
         for elem in tree.findall('//decimalFormats/decimalFormatLength'):
-            if 'draft' in elem.attrib and elem.attrib.get('type') in decimal_formats:
+            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 and elem.attrib.get('type') in scientific_formats:
+            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 and elem.attrib.get('type') in currency_formats:
+            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 and elem.attrib.get('type') in percent_formats:
+            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)
@@ -360,5 +455,6 @@
         finally:
             outfile.close()
 
+
 if __name__ == '__main__':
     main()
Copyright (C) 2012-2017 Edgewall Software