summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--COPYING28
-rw-r--r--ChangeLog1
-rw-r--r--INSTALL.txt37
-rw-r--r--MANIFEST.in3
-rw-r--r--README.txt12
-rw-r--r--babel/__init__.py32
-rw-r--r--babel/catalog/__init__.py72
-rw-r--r--babel/catalog/extract.py214
-rw-r--r--babel/catalog/frontend.py150
-rw-r--r--babel/catalog/pofile.py206
-rw-r--r--babel/catalog/tests/__init__.py24
-rw-r--r--babel/catalog/tests/extract.py25
-rw-r--r--babel/catalog/tests/pofile.py25
-rw-r--r--babel/core.py357
-rw-r--r--babel/dates.py383
-rw-r--r--babel/numbers.py164
-rw-r--r--babel/tests/__init__.py28
-rw-r--r--babel/tests/core.py25
-rw-r--r--babel/tests/dates.py25
-rw-r--r--babel/tests/numbers.py25
-rw-r--r--babel/tests/util.py25
-rw-r--r--babel/util.py183
-rwxr-xr-xscripts/import_cldr.py223
-rwxr-xr-xsetup.py117
24 files changed, 2384 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..74202d4
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,28 @@
+Copyright (C) 2007 Edgewall Software
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ 3. The name of the author may not be used to endorse or promote
+ products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..bb5e1b7
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1 @@
+Nothing so far, not even public yet.
diff --git a/INSTALL.txt b/INSTALL.txt
new file mode 100644
index 0000000..89209f8
--- /dev/null
+++ b/INSTALL.txt
@@ -0,0 +1,37 @@
+Installing Babel
+================
+
+Prerequisites
+-------------
+
+ * Python 2.3 or later (2.4 or later is recommended)
+ * Optional: setuptools 0.6b1 or later
+
+
+Installation
+------------
+
+Once you've downloaded and unpacked a Babel source release, enter the
+directory where the archive was unpacked, and run:
+
+ $ python setup.py install
+
+Note that you may need administrator/root privileges for this step, as
+this command will by default attempt to install Babel to the Python
+site-packages directory on your system.
+
+For advanced options, please refer to the easy_install and/or the distutils
+documentation:
+
+ http://peak.telecommunity.com/DevCenter/EasyInstall
+ http://docs.python.org/inst/inst.html
+
+
+Support
+-------
+
+If you encounter any problems with Babel, please don't hesitate to ask
+questions on the Babel mailing list or IRC channel:
+
+ http://babel.edgewall.org/wiki/MailingList
+ http://babel.edgewall.org/wiki/IrcChannel
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..4fa7a34
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+include babel/localedata/*.dat
+include doc/api/*.*
+include doc/*.html
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..9c55b56
--- /dev/null
+++ b/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/>
diff --git a/babel/__init__.py b/babel/__init__.py
new file mode 100644
index 0000000..cca0636
--- /dev/null
+++ b/babel/__init__.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+"""Integrated collection of utilities that assist in internationalizing and
+localizing applications.
+
+This package is basically composed of two major parts:
+
+ * tools to build and work with ``gettext`` message catalogs
+ * a Python interface to the CLDR (Common Locale Data Repository), providing
+ access to various locale display names, localized number and date
+ formatting, etc.
+
+:see: http://www.gnu.org/software/gettext/
+:see: http://docs.python.org/lib/module-gettext.html
+:see: http://www.unicode.org/cldr/
+"""
+
+from babel.core import Locale
+
+__docformat__ = 'restructuredtext en'
+__version__ = __import__('pkg_resources').get_distribution('Babel').version
diff --git a/babel/catalog/__init__.py b/babel/catalog/__init__.py
new file mode 100644
index 0000000..3f99b35
--- /dev/null
+++ b/babel/catalog/__init__.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+"""Support for ``gettext`` message catalogs."""
+
+import gettext
+
+__all__ = ['Translations']
+
+DEFAULT_DOMAIN = 'messages'
+
+
+class Translations(gettext.GNUTranslations):
+ """An extended translation catalog class."""
+
+ def __init__(self, fileobj=None):
+ """Initialize the translations catalog.
+
+ :param fileobj: the file-like object the translation should be read
+ from
+ """
+ GNUTranslations.__init__(self, fp=fileobj)
+ self.files = [getattr(fileobj, 'name')]
+
+ def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN):
+ """Load translations from the given directory.
+
+ :param dirname: the directory containing the ``MO`` files
+ :param locales: the list of locales in order of preference (items in
+ this list can be either `Locale` objects or locale
+ strings)
+ :param domain: the message domain
+ :return: the loaded catalog, or a ``NullTranslations`` instance if no
+ matching translations were found
+ :rtype: `Translations`
+ """
+ locales = [str(locale) for locale in locales]
+ filename = gettext.find(domain, dirname, locales)
+ if not filename:
+ return NullTranslations()
+ return cls(open(filename, 'rb'))
+ load = classmethod(load)
+
+ def merge(self, translations):
+ """Merge the given translations into the catalog.
+
+ Message translations in the specfied catalog override any messages with
+ the same identifier in the existing catalog.
+
+ :param translations: the `Translations` instance with the messages to
+ merge
+ :return: the `Translations` instance (``self``) so that `merge` calls
+ can be easily chained
+ :rtype: `Translations`
+ """
+ if isinstance(translations, Translations):
+ self._catalog.update(translations._catalog)
+ self.files.extend(translations.files)
+ return self
+
+ def __repr__(self):
+ return "<%s %r>" % (type(self).__name__)
diff --git a/babel/catalog/extract.py b/babel/catalog/extract.py
new file mode 100644
index 0000000..8aaa1fe
--- /dev/null
+++ b/babel/catalog/extract.py
@@ -0,0 +1,214 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+"""Basic infrastructure for extracting localizable messages from source files.
+
+This module defines an extensible system for collecting localizable message
+strings from a variety of sources. A native extractor for Python source files
+is builtin, extractors for other sources can be added using very simple plugins.
+
+The main entry points into the extraction functionality are the functions
+`extract_from_dir` and `extract_from_file`.
+"""
+
+import os
+from pkg_resources import working_set
+import sys
+from tokenize import generate_tokens, NAME, OP, STRING
+
+from babel.util import extended_glob
+
+__all__ = ['extract', 'extract_from_dir', 'extract_from_file']
+__docformat__ = 'restructuredtext en'
+
+GROUP_NAME = 'babel.extractors'
+
+KEYWORDS = (
+ '_', 'gettext', 'ngettext',
+ 'dgettext', 'dngettext',
+ 'ugettext', 'ungettext'
+)
+
+DEFAULT_MAPPING = {
+ 'genshi': ['*.html', '**/*.html'],
+ 'python': ['*.py', '**/*.py']
+}
+
+def extract_from_dir(dirname, mapping=DEFAULT_MAPPING, keywords=KEYWORDS,
+ options=None):
+ """Extract messages from any source files found in the given directory.
+
+ This function generates tuples of the form:
+
+ ``(filename, lineno, funcname, message)``
+
+ Which extraction method used is per file is determined by the `mapping`
+ parameter, which maps extraction method names to lists of extended glob
+ patterns. For example, the following is the default mapping:
+
+ >>> mapping = {
+ ... 'python': ['*.py', '**/*.py']
+ ... }
+
+ This basically says that files with the filename extension ".py" at any
+ level inside the directory should be processed by the "python" extraction
+ method. Files that don't match any of the patterns are ignored.
+
+ The following extended mapping would also use the "genshi" extraction method
+ on any file in "templates" subdirectory:
+
+ >>> mapping = {
+ ... 'genshi': ['**/templates/*.*', '**/templates/**/*.*'],
+ ... 'python': ['*.py', '**/*.py']
+ ... }
+
+ :param dirname: the path to the directory to extract messages from
+ :param mapping: a mapping of extraction method names to extended glob
+ patterns
+ :param keywords: a list of keywords (i.e. function names) that should be
+ recognized as translation functions
+ :param options: a dictionary of additional options (optional)
+ :return: an iterator over ``(filename, lineno, funcname, message)`` tuples
+ :rtype: ``iterator``
+ """
+ extracted_files = {}
+ for method, patterns in mapping.items():
+ for pattern in patterns:
+ for filename in extended_glob(pattern, dirname):
+ if filename in extracted_files:
+ continue
+ filepath = os.path.join(dirname, filename)
+ for line, func, key in extract_from_file(method, filepath,
+ keywords=keywords,
+ options=options):
+ yield filename, line, func, key
+ extracted_files[filename] = True
+
+def extract_from_file(method, filename, keywords=KEYWORDS, options=None):
+ """Extract messages from a specific file.
+
+ This function returns a list of tuples of the form:
+
+ ``(lineno, funcname, message)``
+
+ :param filename: the path to the file to extract messages from
+ :param method: a string specifying the extraction method (.e.g. "python")
+ :param keywords: a list of keywords (i.e. function names) that should be
+ recognized as translation functions
+ :param options: a dictionary of additional options (optional)
+ :return: the list of extracted messages
+ :rtype: `list`
+ """
+ fileobj = open(filename, 'U')
+ try:
+ return list(extract(method, fileobj, keywords, options=options))
+ finally:
+ fileobj.close()
+
+def extract(method, fileobj, keywords=KEYWORDS, options=None):
+ """Extract messages from the given file-like object using the specified
+ extraction method.
+
+ This function returns a list of tuples of the form:
+
+ ``(lineno, funcname, message)``
+
+ The implementation dispatches the actual extraction to plugins, based on the
+ value of the ``method`` parameter.
+
+ >>> source = '''# foo module
+ ... def run(argv):
+ ... print _('Hello, world!')
+ ... '''
+
+ >>> from StringIO import StringIO
+ >>> for message in extract('python', StringIO(source)):
+ ... print message
+ (3, '_', 'Hello, world!')
+
+ :param method: a string specifying the extraction method (.e.g. "python")
+ :param fileobj: the file-like object the messages should be extracted from
+ :param keywords: a list of keywords (i.e. function names) that should be
+ recognized as translation functions
+ :param options: a dictionary of additional options (optional)
+ :return: the list of extracted messages
+ :rtype: `list`
+ :raise ValueError: if the extraction method is not registered
+ """
+ for entry_point in working_set.iter_entry_points(GROUP_NAME, method):
+ func = entry_point.load(require=True)
+ return list(func(fileobj, keywords, options=options or {}))
+ raise ValueError('Unknown extraction method %r' % method)
+
+def extract_genshi(fileobj, keywords, options):
+ """Extract messages from Genshi templates.
+
+ :param fileobj: the file-like object the messages should be extracted from
+ :param keywords: a list of keywords (i.e. function names) that should be
+ recognized as translation functions
+ :param options: a dictionary of additional options (optional)
+ :return: an iterator over ``(lineno, funcname, message)`` tuples
+ :rtype: ``iterator``
+ """
+ from genshi.filters.i18n import Translator
+ from genshi.template import MarkupTemplate
+ tmpl = MarkupTemplate(fileobj, filename=getattr(fileobj, 'name'))
+ translator = Translator(None)
+ for message in translator.extract(tmpl.stream, gettext_functions=keywords):
+ yield message
+
+def extract_python(fileobj, keywords, options):
+ """Extract messages from Python source code.
+
+ :param fileobj: the file-like object the messages should be extracted from
+ :param keywords: a list of keywords (i.e. function names) that should be
+ recognized as translation functions
+ :param options: a dictionary of additional options (optional)
+ :return: an iterator over ``(lineno, funcname, message)`` tuples
+ :rtype: ``iterator``
+ """
+ funcname = None
+ lineno = None
+ buf = []
+ messages = []
+ in_args = False
+
+ tokens = generate_tokens(fileobj.readline)
+ for tok, value, (lineno, _), _, _ in tokens:
+ if funcname and tok == OP and value == '(':
+ in_args = True
+ elif funcname and in_args:
+ if tok == OP and value == ')':
+ in_args = False
+ if buf:
+ messages.append(''.join(buf))
+ del buf[:]
+ if filter(None, messages):
+ if len(messages) > 1:
+ messages = tuple(messages)
+ else:
+ messages = messages[0]
+ yield lineno, funcname, messages
+ funcname = lineno = None
+ messages = []
+ elif tok == STRING:
+ if lineno is None:
+ lineno = stup[0]
+ buf.append(value[1:-1])
+ elif tok == OP and value == ',':
+ messages.append(''.join(buf))
+ del buf[:]
+ elif funcname:
+ funcname = None
+ elif tok == NAME and value in keywords:
+ funcname = value
diff --git a/babel/catalog/frontend.py b/babel/catalog/frontend.py
new file mode 100644
index 0000000..31074d2
--- /dev/null
+++ b/babel/catalog/frontend.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+"""Frontends for the message extraction functionality."""
+
+from distutils import log
+from distutils.cmd import Command
+from optparse import OptionParser
+import os
+import sys
+
+from babel import __version__ as VERSION
+from babel.catalog.extract import extract_from_dir, KEYWORDS
+from babel.catalog.pofile import write_po
+
+__all__ = ['extract_messages', 'main']
+__docformat__ = 'restructuredtext en'
+
+
+class extract_messages(Command):
+ """Message extraction command for use in ``setup.py`` scripts.
+
+ If correctly installed, this command is available to Setuptools-using
+ setup scripts automatically. For projects using plain old ``distutils``,
+ the command needs to be registered explicitly in ``setup.py``::
+
+ from babel.catalog.frontend import extract_messages
+
+ setup(
+ ...
+ cmdclass = {'extract_messages': extract_messages}
+ )
+
+ :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_
+ :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
+ """
+
+ description = 'extract localizable strings from the project code'
+ user_options = [
+ ('charset=', None,
+ 'charset to use in the output file'),
+ ('keywords=', 'k',
+ 'comma-separated list of keywords to look for in addition to the '
+ 'defaults'),
+ ('no-location', None,
+ 'do not include location comments with filename and line number'),
+ ('omit-header', None,
+ 'do not include msgid "" entry in header'),
+ ('output-file=', None,
+ 'name of the output file'),
+ ]
+ boolean_options = ['no-location', 'omit-header']
+
+ def initialize_options(self):
+ self.charset = 'utf-8'
+ self.keywords = KEYWORDS
+ self.no_location = False
+ self.omit_header = False
+ self.output_file = None
+ self.input_dirs = None
+
+ def finalize_options(self):
+ if not self.input_dirs:
+ self.input_dirs = dict.fromkeys([k.split('.',1)[0]
+ for k in self.distribution.packages
+ ]).keys()
+ if isinstance(self.keywords, basestring):
+ new_keywords = [k.strip() for k in self.keywords.split(',')]
+ self.keywords = list(KEYWORDS) + new_keywords
+
+ def run(self):
+ outfile = open(self.output_file, 'w')
+ try:
+ messages = []
+ for dirname in self.input_dirs:
+ log.info('extracting messages from %r' % dirname)
+ extracted = extract_from_dir(dirname, keywords=self.keywords)
+ for filename, lineno, funcname, message in extracted:
+ messages.append((os.path.join(dirname, filename), lineno,
+ funcname, message))
+ write_po(outfile, messages, charset=self.charset,
+ no_location=self.no_location, omit_header=self.omit_header)
+ log.info('writing PO file to %s' % self.output_file)
+ finally:
+ outfile.close()
+
+
+def main(argv=sys.argv):
+ """Command-line interface.
+
+ This function provides a simple command-line interface to the message
+ extraction and PO file generation functionality.
+
+ :param argv: list of arguments passed on the command-line
+ """
+ parser = OptionParser(usage='%prog [options] dirname1 <dirname2> ...',
+ version='%%prog %s' % VERSION)
+ parser.add_option('--charset', dest='charset', default='utf-8',
+ help='charset to use in the output')
+ parser.add_option('-k', '--keyword', dest='keywords',
+ default=list(KEYWORDS), action='append',
+ help='keywords to look for in addition to the defaults. '
+ 'You can specify multiple -k flags on the command '
+ 'line.')
+ parser.add_option('--no-location', dest='no_location', default=False,
+ action='store_true',
+ help='do not include location comments with filename and '
+ 'line number')
+ parser.add_option('--omit-header', dest='omit_header', default=False,
+ action='store_true',
+ help='do not include msgid "" entry in header')
+ parser.add_option('-o', '--output', dest='output',
+ help='path to the output POT file')
+ options, args = parser.parse_args(argv[1:])
+ if not args:
+ parser.error('incorrect number of arguments')
+
+ if options.output not in (None, '-'):
+ outfile = open(options.output, 'w')
+ else:
+ outfile = sys.stdout
+
+ try:
+ messages = []
+ for dirname in args:
+ if not os.path.isdir(dirname):
+ parser.error('%r is not a directory' % dirname)
+ extracted = extract_from_dir(dirname, keywords=options.keywords)
+ for filename, lineno, funcname, message in extracted:
+ messages.append((os.path.join(dirname, filename), lineno,
+ funcname, message))
+ write_po(outfile, messages,
+ charset=options.charset, no_location=options.no_location,
+ omit_header=options.omit_header)
+ finally:
+ if options.output:
+ outfile.close()
+
+if __name__ == '__main__':
+ main()
diff --git a/babel/catalog/pofile.py b/babel/catalog/pofile.py
new file mode 100644
index 0000000..595641e
--- /dev/null
+++ b/babel/catalog/pofile.py
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+"""Reading and writing of files in the ``gettext`` PO (portable object)
+format.
+
+:see: `The Format of PO Files
+ <http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files>`_
+"""
+
+# TODO: line wrapping
+
+from datetime import datetime
+import re
+
+from babel import __version__ as VERSION
+
+__all__ = ['escape', 'normalize', 'read_po', 'write_po']
+
+POT_HEADER = """\
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: %%(project)s %%(version)s\\n"
+"POT-Creation-Date: %%(time)s\\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
+"Language-Team: LANGUAGE <LL@li.org>\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=%%(charset)s\\n"
+"Content-Transfer-Encoding: %%(charset)s\\n"
+"Generated-By: Babel %s\\n"
+
+""" % VERSION
+
+PYTHON_FORMAT = re.compile(r'(\%\(([\w]+)\)[diouxXeEfFgGcrs])').search
+
+def escape(string):
+ r"""Escape the given string so that it can be included in double-quoted
+ strings in ``PO`` files.
+
+ >>> escape('''Say:
+ ... "hello, world!"
+ ... ''')
+ 'Say:\\n \\"hello, world!\\"\\n'
+
+ :param string: the string to escape
+ :return: the escaped string
+ :rtype: `str` or `unicode`
+ """
+ return string.replace('\\', '\\\\') \
+ .replace('\t', '\\t') \
+ .replace('\r', '\\r') \
+ .replace('\n', '\\n') \
+ .replace('\"', '\\"')
+
+def normalize(string, charset='utf-8'):
+ """This converts a string into a format that is appropriate for .po files,
+ namely much closer to C style.
+
+ :param string: the string to normalize
+ :param charset: the encoding to use for `unicode` strings
+ :return: the normalized string
+ :rtype: `str`
+ """
+ string = string.encode(charset, 'backslashreplace')
+ lines = string.split('\n')
+ if len(lines) == 1:
+ string = '"' + escape(string) + '"'
+ else:
+ if not lines[-1]:
+ del lines[-1]
+ lines[-1] = lines[-1] + '\n'
+ for i in range(len(lines)):
+ lines[i] = escape(lines[i])
+ lineterm = '\\n"\n"'
+ string = '""\n"' + lineterm.join(lines) + '"'
+ return string
+
+def read_po(fileobj):
+ """Parse a PO file.
+
+ This function yields tuples of the form:
+
+ ``(message, translation, locations)``
+
+ where:
+
+ * ``message`` is the original (untranslated) message, or a
+ ``(singular, plural)`` tuple for pluralizable messages
+ * ``translation`` is the translation of the message, or a tuple of
+ translations for pluralizable messages
+ * ``locations`` is a sequence of ``(filename, lineno)`` tuples
+
+ :param fileobj: the file-like object to read the PO file from
+ :return: an iterator over ``(message, translation, location)`` tuples
+ :rtype: ``iterator``
+ """
+ for line in fileobj.readlines():
+ line = line.strip()
+ if line.startswith('#'):
+ continue # TODO: process comments
+ else:
+ if line.startswith('msgid_plural'):
+ msg = line[12:].lstrip()
+ elif line.startswith('msgid'):
+ msg = line[5:].lstrip()
+ elif line.startswith('msgstr'):
+ msg = line[6:].lstrip()
+ if msg.startswith('['):
+ pass # plural
+
+def write_po(fileobj, messages, project=None, version=None, creation_date=None,
+ charset='utf-8', no_location=False, omit_header=False):
+ r"""Write a ``gettext`` PO (portable object) file to the given file-like
+ object.
+
+ The `messages` parameter is expected to be an iterable object producing
+ tuples of the form:
+
+ ``(filename, lineno, funcname, message)``
+
+ >>> from StringIO import StringIO
+ >>> buf = StringIO()
+ >>> write_po(buf, [
+ ... ('main.py', 1, None, u'foo'),
+ ... ('main.py', 3, 'ngettext', (u'bar', u'baz'))
+ ... ], omit_header=True)
+
+ >>> print buf.getvalue()
+ #: main.py:1
+ msgid "foo"
+ msgstr ""
+ <BLANKLINE>
+ #: main.py:3
+ msgid "bar"
+ msgid_plural "baz"
+ msgstr[0] ""
+ msgstr[1] ""
+ <BLANKLINE>
+ <BLANKLINE>
+
+ :param fileobj: the file-like object to write to
+ :param messages: an iterable over the messages
+ :param project: the project name
+ :param version: the project version
+ :param charset: the encoding
+ :param no_location: do not emit a location comment for every message
+ :param omit_header: do not include the ``msgid ""`` entry at the top of the
+ output
+ """
+ def _normalize(key):
+ return normalize(key, charset=charset)
+
+ if creation_date is None:
+ creation_date = datetime.now()
+
+ if not omit_header:
+ fileobj.write(POT_HEADER % {
+ 'charset': charset,
+ 'time': creation_date.strftime('%Y-%m-%d %H:%M'),
+ 'project': project,
+ 'version': version
+ })
+
+ locations = {}
+ msgids = []
+
+ for filename, lineno, funcname, key in messages:
+ if key in msgids:
+ locations[key].append((filename, lineno))
+ else:
+ locations[key] = [(filename, lineno)]
+ msgids.append(key)
+
+ for msgid in msgids:
+ if not no_location:
+ for filename, lineno in locations[msgid]:
+ fileobj.write('#: %s:%s\n' % (filename, lineno))
+ if type(msgid) is tuple:
+ assert len(msgid) == 2
+ if PYTHON_FORMAT(msgid[0]) or PYTHON_FORMAT(msgid[1]):
+ fileobj.write('#, python-format\n')
+ fileobj.write('msgid %s\n' % normalize(msgid[0], charset))
+ fileobj.write('msgid_plural %s\n' % normalize(msgid[1], charset))
+ fileobj.write('msgstr[0] ""\n')
+ fileobj.write('msgstr[1] ""\n')
+ else:
+ if PYTHON_FORMAT(msgid):
+ fileobj.write('#, python-format\n')
+ fileobj.write('msgid %s\n' % normalize(msgid, charset))
+ fileobj.write('msgstr ""\n')
+ fileobj.write('\n')
diff --git a/babel/catalog/tests/__init__.py b/babel/catalog/tests/__init__.py
new file mode 100644
index 0000000..a371f64
--- /dev/null
+++ b/babel/catalog/tests/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+import unittest
+
+def suite():
+ from babel.catalog.tests import extract, pofile
+ suite = unittest.TestSuite()
+ suite.addTest(extract.suite())
+ suite.addTest(pofile.suite())
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/babel/catalog/tests/extract.py b/babel/catalog/tests/extract.py
new file mode 100644
index 0000000..8c9fcfb
--- /dev/null
+++ b/babel/catalog/tests/extract.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+import doctest
+import unittest
+
+from babel.catalog import extract
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocTestSuite(extract))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/babel/catalog/tests/pofile.py b/babel/catalog/tests/pofile.py
new file mode 100644
index 0000000..c8958e0
--- /dev/null
+++ b/babel/catalog/tests/pofile.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+import doctest
+import unittest
+
+from babel.catalog import pofile
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocTestSuite(pofile))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/babel/core.py b/babel/core.py
new file mode 100644
index 0000000..607ea70
--- /dev/null
+++ b/babel/core.py
@@ -0,0 +1,357 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+"""Core locale representation and locale data access gateway."""
+
+import pickle
+from pkg_resources import resource_filename
+try:
+ import threading
+except ImportError:
+ import dummy_threading as threading
+
+__all__ = ['Locale', 'negotiate', 'parse']
+__docformat__ = 'restructuredtext en'
+
+
+class Locale(object):
+ """Representation of a specific locale.
+
+ >>> locale = Locale('en', territory='US')
+ >>> repr(locale)
+ '<Locale "en_US">'
+ >>> locale.display_name
+ u'English (United States)'
+
+ A `Locale` object can also be instantiated from a raw locale string:
+
+ >>> locale = Locale.parse('en-US', sep='-')
+ >>> repr(locale)
+ '<Locale "en_US">'
+
+ `Locale` objects provide access to a collection of locale data, such as
+ territory and language names, number and date format patterns, and more:
+
+ >>> locale.number_symbols['decimal']
+ u'.'
+
+ :see: `IETF RFC 3066 <http://www.ietf.org/rfc/rfc3066.txt>`_
+ """
+ _cache = {}
+ _cache_lock = threading.Lock()
+
+ def __new__(cls, language, territory=None, variant=None):
+ """Create new locale object, or load it from the cache if it had already
+ been instantiated.
+
+ >>> l1 = Locale('en')
+ >>> l2 = Locale('en')
+ >>> l1 is l2
+ True
+
+ :param language: the language code
+ :param territory: the territory (country or region) code
+ :param variant: the variant code
+ :return: new or existing `Locale` instance
+ :rtype: `Locale`
+ """
+ key = (language, territory, variant)
+ cls._cache_lock.acquire()
+ try:
+ self = cls._cache.get(key)
+ if self is None:
+ self = super(Locale, cls).__new__(cls, language, territory,
+ variant)
+ cls._cache[key] = self
+ return self
+ finally:
+ self._cache_lock.release()
+
+ def __init__(self, language, territory=None, variant=None):
+ """Initialize the locale object from the given identifier components.
+
+ >>> locale = Locale('en', 'US')
+ >>> locale.language
+ 'en'
+ >>> locale.territory
+ 'US'
+
+ :param language: the language code
+ :param territory: the territory (country or region) code
+ :param variant: the variant code
+ """
+ self.language = language
+ self.territory = territory
+ self.variant = variant
+ self.__data = None
+
+ def parse(cls, identifier, sep='_'):
+ """Create a `Locale` instance for the given locale identifier.
+
+ >>> l = Locale.parse('de-DE', sep='-')
+ >>> l.display_name
+ u'Deutsch (Deutschland)'
+
+ If the `identifier` parameter is not a string, but actually a `Locale`
+ object, that object is returned:
+
+ >>> Locale.parse(l)
+ <Locale "de_DE">
+
+ :param identifier: the locale identifier string
+ :param sep: optional component separator
+ :return: a corresponding `Locale` instance
+ :rtype: `Locale`
+ :raise `ValueError`: if the string does not appear to be a valid locale
+ identifier
+ """
+ if type(identifier) is cls:
+ return identifier
+ return cls(*parse(identifier, sep=sep))
+ parse = classmethod(parse)
+
+ def __repr__(self):
+ return '<Locale "%s">' % str(self)
+
+ def __str__(self):
+ return '_'.join(filter(None, [self.language, self.territory,
+ self.variant]))
+
+ def _data(self):
+ if self.__data is None:
+ filename = resource_filename(__name__, 'localedata/%s.dat' % self)
+ fileobj = open(filename, 'rb')
+ try:
+ self.__data = pickle.load(fileobj)
+ finally:
+ fileobj.close()
+ return self.__data
+ _data = property(_data)
+
+ def display_name(self):
+ retval = self.languages.get(self.language)
+ if self.territory:
+ variant = ''
+ if self.variant:
+ variant = ', %s' % self.variants.get(self.variant)
+ retval += ' (%s%s)' % (self.territories.get(self.territory), variant)
+ return retval
+ display_name = property(display_name, doc="""\
+ The localized display name of the locale.
+
+ >>> Locale('en').display_name
+ u'English'
+ >>> Locale('en', 'US').display_name
+ u'English (United States)'
+
+ :type: `unicode`
+ """)
+
+ def languages(self):
+ return self._data['languages']
+ languages = property(languages, doc="""\
+ Mapping of language codes to translated language names.
+
+ >>> Locale('de', 'DE').languages['ja']
+ u'Japanisch'
+
+ :type: `dict`
+ :see: `ISO 639 <http://www.loc.gov/standards/iso639-2/>`_
+ """)
+
+ def scripts(self):
+ return self._data['scripts']
+ scripts = property(scripts, doc="""\
+ Mapping of script codes to translated script names.
+
+ >>> Locale('en', 'US').scripts['Hira']
+ u'Hiragana'
+
+ :type: `dict`
+ :see: `ISO 15924 <http://www.evertype.com/standards/iso15924/>`_
+ """)
+
+ def territories(self):
+ return self._data['territories']
+ territories = property(territories, doc="""\
+ Mapping of script codes to translated script names.
+
+ >>> Locale('es', 'CO').territories['DE']
+ u'Alemania'
+
+ :type: `dict`
+ :see: `ISO 3166 <http://www.iso.org/iso/en/prods-services/iso3166ma/>`_
+ """)
+
+ def variants(self):
+ return self._data['variants']
+ variants = property(variants, doc="""\
+ Mapping of script codes to translated script names.
+
+ >>> Locale('de', 'DE').variants['1901']
+ u'alte deutsche Rechtschreibung'
+
+ :type: `dict`
+ """)
+
+ def number_symbols(self):
+ return self._data['number_symbols']
+ number_symbols = property(number_symbols, doc="""\
+ Symbols used in number formatting.
+
+ >>> Locale('fr', 'FR').number_symbols['decimal']
+ u','
+
+ :type: `dict`
+ """)
+
+ def periods(self):
+ return self._data['periods']
+ periods = property(periods, doc="""\
+ Locale display names for day periods (AM/PM).
+
+ >>> Locale('en', 'US').periods['am']
+ u'AM'
+
+ :type: `dict`
+ """)
+
+ def days(self):
+ return self._data['days']
+ days = property(days, doc="""\
+ Locale display names for weekdays.
+
+ >>> Locale('de', 'DE').days['format']['wide'][4]
+ u'Donnerstag'
+
+ :type: `dict`
+ """)
+
+ def months(self):
+ return self._data['months']
+ months = property(months, doc="""\
+ Locale display names for months.
+
+ >>> Locale('de', 'DE').months['format']['wide'][10]
+ u'Oktober'
+
+ :type: `dict`
+ """)
+
+ def quarters(self):
+ return self._data['quarters']
+ quarters = property(quarters, doc="""\
+ Locale display names for quarters.
+
+ >>> Locale('de', 'DE').quarters['format']['wide'][1]
+ u'1. Quartal'
+
+ :type: `dict`
+ """)
+
+ def eras(self):
+ return self._data['eras']
+ eras = property(eras, doc="""\
+ Locale display names for eras.
+
+ >>> Locale('en', 'US').eras['wide'][1]
+ u'Anno Domini'
+ >>> Locale('en', 'US').eras['abbreviated'][0]
+ u'BC'
+
+ :type: `dict`
+ """)
+
+ def date_formats(self):
+ return self._data['date_formats']
+ date_formats = property(date_formats, doc="""\
+ Locale patterns for date formatting.
+
+ >>> Locale('en', 'US').date_formats['short']
+ <DateTimeFormatPattern u'M/d/yy'>
+ >>> Locale('fr', 'FR').date_formats['long']
+ <DateTimeFormatPattern u'd MMMM yyyy'>
+
+ :type: `dict`
+ """)
+
+ def time_formats(self):
+ return self._data['time_formats']
+ time_formats = property(time_formats, doc="""\
+ Locale patterns for time formatting.
+
+ >>> Locale('en', 'US').time_formats['short']
+ <DateTimeFormatPattern u'h:mm a'>
+ >>> Locale('fr', 'FR').time_formats['long']
+ <DateTimeFormatPattern u'HH:mm:ss z'>
+
+ :type: `dict`
+ """)
+
+
+def negotiate(preferred, available):
+ """Find the best match between available and requested locale strings.
+
+ >>> negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT'])
+ 'de_DE'
+ >>> negotiate(['de_DE', 'en_US'], ['en', 'de'])
+ 'de'
+
+ :param preferred: the list of locale strings preferred by the user
+ :param available: the list of locale strings available
+ :return: the locale identifier for the best match, or `None` if no match
+ was found
+ :rtype: `str`
+ """
+ for locale in preferred:
+ if locale in available:
+ return locale
+ parts = locale.split('_')
+ if len(parts) > 1 and parts[0] in available:
+ return parts[0]
+ return None
+
+def parse(identifier, sep='_'):
+ """Parse a locale identifier into a ``(language, territory, variant)``
+ tuple.
+
+ >>> parse('zh_CN')
+ ('zh', 'CN', None)
+
+ The default component separator is "_", but a different separator can be
+ specified using the `sep` parameter:
+
+ >>> parse('zh-CN', sep='-')
+ ('zh', 'CN', None)
+
+ :param identifier: the locale identifier string
+ :param sep: character that separates the different parts of the locale
+ string
+ :return: the ``(language, territory, variant)`` tuple
+ :rtype: `tuple`
+ :raise `ValueError`: if the string does not appear to be a valid locale
+ identifier
+
+ :see: `IETF RFC 3066 <http://www.ietf.org/rfc/rfc3066.txt>`_
+ """
+ parts = identifier.split(sep)
+ lang, territory, variant = parts[0].lower(), None, None
+ if not lang.isalpha():
+ raise ValueError('expected only letters, got %r' % lang)
+ if len(parts) > 1:
+ territory = parts[1].upper().split('.', 1)[0]
+ if not territory.isalpha():
+ raise ValueError('expected only letters, got %r' % territory)
+ if len(parts) > 2:
+ variant = parts[2].upper().split('.', 1)[0]
+ return lang, territory, variant
diff --git a/babel/dates.py b/babel/dates.py
new file mode 100644
index 0000000..4c6406d
--- /dev/null
+++ b/babel/dates.py
@@ -0,0 +1,383 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+"""Locale dependent formatting and parsing of dates and times.
+
+The default locale for the functions in this module is determined by the
+following environment variables, in that order:
+
+ * ``LC_TIME``,
+ * ``LC_ALL``, and
+ * ``LANG``
+"""
+
+from datetime import date, datetime, time
+
+from babel.core import Locale
+from babel.util import default_locale
+
+__all__ = ['format_date', 'format_datetime', 'format_time', 'parse_date',
+ 'parse_datetime', 'parse_time']
+__docformat__ = 'restructuredtext en'
+
+LC_TIME = default_locale('LC_TIME')
+
+def get_period_names(locale=LC_TIME):
+ """Return the names for day periods (AM/PM) used by the locale.
+
+ >>> get_period_names(locale='en_US')['am']
+ u'AM'
+
+ :param locale: the `Locale` object, or a locale string
+ :return: the dictionary of period names
+ :rtype: `dict`
+ """
+ return Locale.parse(locale).periods
+
+def get_day_names(width='wide', context='format', locale=LC_TIME):
+ """Return the day names used by the locale for the specified format.
+
+ >>> get_day_names('wide', locale='en_US')[1]
+ u'Monday'
+ >>> get_day_names('abbreviated', locale='es')[1]
+ u'lun'
+ >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1]
+ u'M'
+
+ :param width: the width to use, one of "wide", "abbreviated", or "narrow"
+ :param context: the context, either "format" or "stand-alone"
+ :param locale: the `Locale` object, or a locale string
+ :return: the dictionary of day names
+ :rtype: `dict`
+ """
+ return Locale.parse(locale).days[context][width]
+
+def get_month_names(width='wide', context='format', locale=LC_TIME):
+ """Return the month names used by the locale for the specified format.
+
+ >>> get_month_names('wide', locale='en_US')[1]
+ u'January'
+ >>> get_month_names('abbreviated', locale='es')[1]
+ u'ene'
+ >>> get_month_names('narrow', context='stand-alone', locale='de_DE')[1]
+ u'J'
+
+ :param width: the width to use, one of "wide", "abbreviated", or "narrow"
+ :param context: the context, either "format" or "stand-alone"
+ :param locale: the `Locale` object, or a locale string
+ :return: the dictionary of month names
+ :rtype: `dict`
+ """
+ return Locale.parse(locale).months[context][width]
+
+def get_quarter_names(width='wide', context='format', locale=LC_TIME):
+ """Return the quarter names used by the locale for the specified format.
+
+ >>> get_quarter_names('wide', locale='en_US')[1]
+ u'1st quarter'
+ >>> get_quarter_names('abbreviated', locale='de_DE')[1]
+ u'Q1'
+
+ :param width: the width to use, one of "wide", "abbreviated", or "narrow"
+ :param context: the context, either "format" or "stand-alone"
+ :param locale: the `Locale` object, or a locale string
+ :return: the dictionary of quarter names
+ :rtype: `dict`
+ """
+ return Locale.parse(locale).quarters[context][width]
+
+def get_era_names(width='wide', locale=LC_TIME):
+ """Return the era names used by the locale for the specified format.
+
+ >>> get_era_names('wide', locale='en_US')[1]
+ u'Anno Domini'
+ >>> get_era_names('abbreviated', locale='de_DE')[1]
+ u'n. Chr.'
+
+ :param width: the width to use, either "wide" or "abbreviated"
+ :param locale: the `Locale` object, or a locale string
+ :return: the dictionary of era names
+ :rtype: `dict`
+ """
+ return Locale.parse(locale).eras[width]
+
+def get_date_format(format='medium', locale=LC_TIME):
+ """Return the date formatting patterns used by the locale for the specified
+ format.
+
+ >>> get_date_format(locale='en_US')
+ <DateTimeFormatPattern u'MMM d, yyyy'>
+ >>> get_date_format('full', locale='de_DE')
+ <DateTimeFormatPattern u'EEEE, d. MMMM yyyy'>
+
+ :param format: the format to use, one of "full", "long", "medium", or
+ "short"
+ :param locale: the `Locale` object, or a locale string
+ :return: the date format pattern
+ :rtype: `dict`
+ """
+ return Locale.parse(locale).date_formats[format]
+
+def get_time_format(format='medium', locale=LC_TIME):
+ """Return the time formatting patterns used by the locale for the specified
+ format.
+
+ >>> get_time_format(locale='en_US')
+ <DateTimeFormatPattern u'h:mm:ss a'>
+ >>> get_time_format('full', locale='de_DE')
+ <DateTimeFormatPattern u"H:mm' Uhr 'z">
+
+ :param format: the format to use, one of "full", "long", "medium", or
+ "short"
+ :param locale: the `Locale` object, or a locale string
+ :return: the time format pattern
+ :rtype: `dict`
+ """
+ return Locale.parse(locale).time_formats[format]
+
+def format_date(date, format='medium', locale=LC_TIME):
+ """Returns a date formatted according to the given pattern.
+
+ >>> d = date(2007, 04, 01)
+ >>> format_date(d, locale='en_US')
+ u'Apr 1, 2007'
+ >>> format_date(d, format='full', locale='de_DE')
+ u'Sonntag, 1. April 2007'
+
+ :param date: the ``date`` object
+ :param format: one of "full", "long", "medium", or "short"
+ :param locale: a `Locale` object or a locale string
+ :rtype: `unicode`
+ """
+ locale = Locale.parse(locale)
+ if format in ('full', 'long', 'medium', 'short'):
+ format = get_date_format(format, locale=locale)
+ pattern = parse_pattern(format)
+ return parse_pattern(format).apply(date, locale)
+
+def format_datetime(datetime, format='medium', locale=LC_TIME):
+ """Returns a date formatted according to the given pattern.
+
+ :param datetime: the ``date`` object
+ :param format: one of "full", "long", "medium", or "short"
+ :param locale: a `Locale` object or a locale string
+ :rtype: `unicode`
+ """
+ raise NotImplementedError
+
+def format_time(time, format='medium', locale=LC_TIME):
+ """Returns a time formatted according to the given pattern.
+
+ >>> t = time(15, 30)
+ >>> format_time(t, locale='en_US')
+ u'3:30:00 PM'
+ >>> format_time(t, format='short', locale='de_DE')
+ u'15:30'
+
+ :param time: the ``time`` object
+ :param format: one of "full", "long", "medium", or "short"
+ :param locale: a `Locale` object or a locale string
+ :rtype: `unicode`
+ """
+ locale = Locale.parse(locale)
+ if format in ('full', 'long', 'medium', 'short'):
+ format = get_time_format(format, locale=locale)
+ return parse_pattern(format).apply(time, locale)
+
+def parse_date(string, locale=LC_TIME):
+ raise NotImplementedError
+
+def parse_datetime(string, locale=LC_TIME):
+ raise NotImplementedError
+
+def parse_time(string, locale=LC_TIME):
+ raise NotImplementedError
+
+
+class DateTimeFormatPattern(object):
+
+ def __init__(self, pattern, format):
+ self.pattern = pattern
+ self.format = format
+
+ def __repr__(self):
+ return '<%s %r>' % (type(self).__name__, self.pattern)
+
+ def __unicode__(self):
+ return self.pattern
+
+ def __mod__(self, other):
+ assert type(other) is DateTimeFormat
+ return self.format % other
+
+ def apply(self, datetime, locale):
+ return self % DateTimeFormat(datetime, locale)
+
+
+class DateTimeFormat(object):
+
+ def __init__(self, value, locale):
+ assert isinstance(value, (date, datetime, time))
+ self.value = value
+ self.locale = Locale.parse(locale)
+
+ def __getitem__(self, name):
+ # TODO: a number of fields missing here
+ if name[0] == 'G':
+ return self.format_era(len(name))
+ elif name[0] == 'y':
+ return self.format_year(self.value.year, len(name))
+ elif name[0] == 'Y':
+ return self.format_year(self.value.isocalendar()[0], len(name))
+ elif name[0] == 'Q':
+ return self.format_quarter(len(name))
+ elif name[0] == 'q':
+ return self.format_quarter(len(name), context='stand-alone')
+ elif name[0] == 'M':
+ return self.format_month(len(name))
+ elif name[0] == 'L':
+ return self.format_month(len(name), context='stand-alone')
+ elif name[0] == 'd':
+ return self.format(self.value.day, len(name))
+ elif name[0] == 'E':
+ return self.format_weekday(len(name))
+ elif name[0] == 'c':
+ return self.format_weekday(len(name), context='stand-alone')
+ elif name[0] == 'a':
+ return self.format_period()
+ elif name[0] == 'h':
+ return self.format(self.value.hour % 12, len(name))
+ elif name[0] == 'H':
+ return self.format(self.value.hour, len(name))
+ elif name[0] == 'm':
+ return self.format(self.value.minute, len(name))
+ elif name[0] == 's':
+ return self.format(self.value.second, len(name))
+ else:
+ raise KeyError('Unsupported date/time field %r' % name[0])
+
+ def format_era(self, num):
+ width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)]
+ era = int(self.value.year >= 0)
+ return get_era_names(width, self.locale)[era]
+
+ def format_year(self, value, num):
+ year = self.format(value, num)
+ if num == 2:
+ year = year[-2:]
+ return year
+
+ def format_month(self, num, context='format'):
+ if num <= 2:
+ return ('%%0%dd' % num) % self.value.month
+ width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num]
+ return get_month_names(width, context, self.locale)[self.value.month]
+
+ def format_weekday(self, num, context='format'):
+ width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)]
+ weekday = self.value.weekday() + 1
+ return get_day_names(width, context, self.locale)[weekday]
+
+ def format_period(self):
+ period = {0: 'am', 1: 'pm'}[int(self.value.hour > 12)]
+ return get_period_names(locale=self.locale)[period]
+
+ def format(self, value, length):
+ return ('%%0%dd' % length) % value
+
+
+PATTERN_CHARS = {
+ 'G': 5, # era
+ 'y': None, 'Y': None, 'u': None, # year
+ 'Q': 4, 'q': 4, # quarter
+ 'M': 5, 'L': 5, # month
+ 'w': 2, 'W': 1, # week
+ 'd': 2, 'D': 3, 'F': 1, 'g': None, # day
+ 'E': 5, 'e': 5, 'c': 5, # week day
+ 'a': 1, # period
+ 'h': 2, 'H': 2, 'K': 2, 'k': 2, # hour
+ 'm': 2, # minute
+ 's': 2, 'S': None, 'A': None, # second
+ 'z': 4, 'Z': 4, 'v': 4 # zone
+}
+
+def parse_pattern(pattern):
+ """Parse date, time, and datetime format patterns.
+
+ >>> parse_pattern("MMMMd").format
+ u'%(MMMM)s%(d)s'
+ >>> parse_pattern("MMM d, yyyy").format
+ u'%(MMM)s %(d)s, %(yyyy)s'
+ >>> parse_pattern("H:mm' Uhr 'z").format
+ u'%(H)s:%(mm)s Uhr %(z)s'
+
+ :param pattern: the formatting pattern to parse
+ """
+ if type(pattern) is DateTimeFormatPattern:
+ return pattern
+
+ result = []
+ quotebuf = None
+ charbuf = []
+ fieldchar = ['']
+ fieldnum = [0]
+
+ def append_chars():
+ result.append(''.join(charbuf).replace('%', '%%'))
+ del charbuf[:]
+
+ def append_field():
+ limit = PATTERN_CHARS[fieldchar[0]]
+ if limit is not None and fieldnum[0] > limit:
+ raise ValueError('Invalid length for field: %r'
+ % (fieldchar[0] * fieldnum[0]))
+ result.append('%%(%s)s' % (fieldchar[0] * fieldnum[0]))
+ fieldchar[0] = ''
+ fieldnum[0] = 0
+
+ for idx, char in enumerate(pattern):
+ if quotebuf is None:
+ if char == "'": # quote started
+ if fieldchar[0]:
+ append_field()
+ elif charbuf:
+ append_chars()
+ quotebuf = []
+ elif char in PATTERN_CHARS:
+ if charbuf:
+ append_chars()
+ if char == fieldchar[0]:
+ fieldnum[0] += 1
+ else:
+ if fieldchar[0]:
+ append_field()
+ fieldchar[0] = char
+ fieldnum[0] = 1
+ else:
+ if fieldchar[0]:
+ append_field()
+ charbuf.append(char)
+
+ elif quotebuf is not None:
+ if char == "'": # quote ended
+ charbuf.extend(quotebuf)
+ quotebuf = None
+ else: # inside quote
+ quotebuf.append(char)
+
+ if fieldchar[0]:
+ append_field()
+ elif charbuf:
+ append_chars()
+
+ return DateTimeFormatPattern(pattern, u''.join(result))
diff --git a/babel/numbers.py b/babel/numbers.py
new file mode 100644
index 0000000..ec9f83e
--- /dev/null
+++ b/babel/numbers.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+"""Locale dependent formatting and parsing of numeric data.
+
+The default locale for the functions in this module is determined by the
+following environment variables, in that order:
+
+ * ``LC_NUMERIC``,
+ * ``LC_ALL``, and
+ * ``LANG``
+"""
+# TODO: percent and scientific formatting
+
+import re
+
+from babel.core import Locale
+from babel.util import default_locale
+
+__all__ = ['format_number', 'format_decimal', 'format_currency',
+ 'format_percent', 'format_scientific', 'parse_number',
+ 'parse_decimal']
+__docformat__ = 'restructuredtext en'
+
+LC_NUMERIC = default_locale('LC_NUMERIC')
+
+def get_decimal_symbol(locale=LC_NUMERIC):
+ """Return the symbol used by the locale to separate decimal fractions.
+
+ >>> get_decimal_symbol('en_US')
+ u'.'
+
+ :param locale: the `Locale` object or locale identifier
+ :return: the decimal symbol
+ :rtype: `unicode`
+ """
+ return Locale.parse(locale).number_symbols.get('decimal', u'.')
+
+def get_group_symbol(locale=LC_NUMERIC):
+ """Return the symbol used by the locale to separate groups of thousands.
+
+ >>> get_group_symbol('en_US')
+ u','
+
+ :param locale: the `Locale` object or locale identifier
+ :return: the group symbol
+ :rtype: `unicode`
+ """
+ return Locale.parse(locale).number_symbols.get('group', u'.')
+
+def format_number(number, locale=LC_NUMERIC):
+ """Returns the given number formatted for a specific locale.
+
+ >>> format_number(1099, locale='en_US')
+ u'1,099'
+
+ :param number: the number to format
+ :param locale: the `Locale` object or locale identifier
+ :return: the formatted number
+ :rtype: `unicode`
+ """
+ group = get_group_symbol(locale)
+ if not group:
+ return unicode(number)
+ thou = re.compile(r'([0-9])([0-9][0-9][0-9]([%s]|$))' % group).search
+ v = str(number)
+ mo = thou(v)
+ while mo is not None:
+ l = mo.start(0)
+ v = v[:l+1] + group + v[l+1:]
+ mo = thou(v)
+ return unicode(v)
+
+def format_decimal(number, places=2, locale=LC_NUMERIC):
+ """Returns the given decimal number formatted for a specific locale.
+
+ >>> format_decimal(1099.98, locale='en_US')
+ u'1,099.98'
+
+ The appropriate thousands grouping and the decimal separator are used for
+ each locale:
+
+ >>> format_decimal(1099.98, locale='de_DE')
+ u'1.099,98'
+
+ The number of decimal places defaults to 2, but can also be specified
+ explicitly:
+
+ >>> format_decimal(1099.98, places=4, locale='en_US')
+ u'1,099.9800'
+
+ :param number: the number to format
+ :param places: the number of digit behind the decimal point
+ :param locale: the `Locale` object or locale identifier
+ :return: the formatted decimal number
+ :rtype: `unicode`
+ """
+ locale = Locale.parse(locale)
+ a, b = (('%%.%df' % places) % number).split('.')
+ return unicode(format_number(a, locale) + get_decimal_symbol(locale) + b)
+
+def format_currency(value, locale=LC_NUMERIC):
+ """Returns formatted currency value.
+
+ >>> format_currency(1099.98, locale='en_US')
+ u'1,099.98'
+
+ :param value: the number to format
+ :param locale: the `Locale` object or locale identifier
+ :return: the formatted currency value
+ :rtype: `unicode`
+ """
+ return format_decimal(value, places=2, locale=locale)
+
+def format_percent(value, places=2, locale=LC_NUMERIC):
+ raise NotImplementedError
+
+def format_scientific(value, locale=LC_NUMERIC):
+ raise NotImplementedError
+
+def parse_number(string, locale=LC_NUMERIC):
+ """Parse localized number string into a long integer.
+
+ >>> parse_number('1,099', locale='en_US')
+ 1099L
+ >>> parse_number('1.099', locale='de_DE')
+ 1099L
+
+ :param string: the string to parse
+ :param locale: the `Locale` object or locale identifier
+ :return: the parsed number
+ :rtype: `long`
+ :raise `ValueError`: if the string can not be converted to a number
+ """
+ return long(string.replace(get_group_symbol(locale), ''))
+
+def parse_decimal(string, locale=LC_NUMERIC):
+ """Parse localized decimal string into a float.
+
+ >>> parse_decimal('1,099.98', locale='en_US')
+ 1099.98
+ >>> parse_decimal('1.099,98', locale='de_DE')
+ 1099.98
+
+ :param string: the string to parse
+ :param locale: the `Locale` object or locale identifier
+ :return: the parsed decimal number
+ :rtype: `float`
+ :raise `ValueError`: if the string can not be converted to a decimal number
+ """
+ locale = Locale.parse(locale)
+ string = string.replace(get_group_symbol(locale), '') \
+ .replace(get_decimal_symbol(locale), '.')
+ return float(string)
diff --git a/babel/tests/__init__.py b/babel/tests/__init__.py
new file mode 100644
index 0000000..fa6e2cc
--- /dev/null
+++ b/babel/tests/__init__.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+import unittest
+
+def suite():
+ from babel.tests import core, dates, numbers, util
+ from babel.catalog import tests as catalog
+ suite = unittest.TestSuite()
+ suite.addTest(core.suite())
+ suite.addTest(dates.suite())
+ suite.addTest(numbers.suite())
+ suite.addTest(util.suite())
+ suite.addTest(catalog.suite())
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/babel/tests/core.py b/babel/tests/core.py
new file mode 100644
index 0000000..1337cf6
--- /dev/null
+++ b/babel/tests/core.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+import doctest
+import unittest
+
+from babel import core
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocTestSuite(core))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/babel/tests/dates.py b/babel/tests/dates.py
new file mode 100644
index 0000000..e614d85
--- /dev/null
+++ b/babel/tests/dates.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+import doctest
+import unittest
+
+from babel import dates
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocTestSuite(dates))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/babel/tests/numbers.py b/babel/tests/numbers.py
new file mode 100644
index 0000000..68a9dc4
--- /dev/null
+++ b/babel/tests/numbers.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+import doctest
+import unittest
+
+from babel import numbers
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocTestSuite(numbers))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/babel/tests/util.py b/babel/tests/util.py
new file mode 100644
index 0000000..0c780d3
--- /dev/null
+++ b/babel/tests/util.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+import doctest
+import unittest
+
+from babel import util
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocTestSuite(util))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/babel/util.py b/babel/util.py
new file mode 100644
index 0000000..b2b7725
--- /dev/null
+++ b/babel/util.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+"""Various utility classes and functions."""
+
+import os
+import re
+
+__all__ = ['default_locale', 'extended_glob', 'lazy']
+__docformat__ = 'restructuredtext en'
+
+def default_locale(kind):
+ """Returns the default locale for a given category, based on environment
+ variables.
+
+ :param kind: one of the ``LC_XXX`` environment variable names
+ :return: the value of the variable, or any of the fallbacks (``LC_ALL`` and
+ ``LANG``)
+ :rtype: `str`
+ """
+ for name in (kind, 'LC_ALL', 'LANG'):
+ locale = os.getenv(name)
+ if locale is not None:
+ return locale
+
+def extended_glob(pattern, dirname=''):
+ """Extended pathname pattern expansion.
+
+ This function is similar to what is provided by the ``glob`` module in the
+ Python standard library, but also supports a convenience pattern ("**") to
+ match files at any directory level.
+
+ :param pattern: the glob pattern
+ :param dirname: the path to the directory in which to search for files
+ matching the given pattern
+ :return: an iterator over the absolute filenames of any matching files
+ :rtype: ``iterator``
+ """
+ symbols = {
+ '?': '[^/]',
+ '?/': '[^/]/',
+ '*': '[^/]+',
+ '*/': '[^/]+/',
+ '**': '(?:.+/)*?',
+ '**/': '(?:.+/)*?',
+ }
+ buf = []
+ for idx, part in enumerate(re.split('([?*]+/?)', pattern)):
+ if idx % 2:
+ buf.append(symbols[part])
+ elif part:
+ buf.append(re.escape(part))
+ regex = re.compile(''.join(buf) + '$')
+
+ absname = os.path.abspath(dirname)
+ for root, dirnames, filenames in os.walk(absname):
+ for subdir in dirnames:
+ if subdir.startswith('.') or subdir.startswith('_'):
+ dirnames.remove(subdir)
+ for filename in filenames:
+ filepath = relpath(
+ os.path.join(root, filename).replace(os.sep, '/'),
+ dirname
+ )
+ if regex.match(filepath):
+ yield filepath
+
+def lazy(func):
+ """Return a new function that lazily evaluates another function.
+
+ >>> lazystr = lazy(str)
+ >>> ls = lazystr('foo')
+ >>> print ls
+ foo
+
+ :param func: the function to wrap
+ :return: a lazily-evaluated version of the function
+ :rtype: ``function``
+ """
+ def newfunc(*args, **kwargs):
+ return LazyProxy(func, *args, **kwargs)
+ return newfunc
+
+
+class LazyProxy(object):
+ """
+
+ >>> lazystr = LazyProxy(str, 'bar')
+ >>> print lazystr
+ bar
+ >>> u'foo' + lazystr
+ u'foobar'
+ """
+
+ def __init__(self, func, *args, **kwargs):
+ self.func = func
+ self.args = args
+ self.kwargs = kwargs
+ self._value = None
+
+ def value(self):
+ if self._value is None:
+ self._value = self.func(*self.args, **self.kwargs)
+ return self._value
+ value = property(value)
+
+ def __str__(self):
+ return str(self.value)
+
+ def __unicode__(self):
+ return unicode(self.value)
+
+ def __add__(self, other):
+ return self.value + other
+
+ def __radd__(self, other):
+ return other + self.value
+
+ def __mod__(self, other):
+ return self.value % other
+
+ def __rmod__(self, other):
+ return other % self.value
+
+ def __mul__(self, other):
+ return self.value * other
+
+ def __rmul__(self, other):
+ return other * self.value
+
+ def __call__(self, *args, **kwargs):
+ return self.value(*args, **kwargs)
+
+ def __cmp__(self, other):
+ return cmp(self.value, other)
+
+ def __rcmp__(self, other):
+ return other + self.value
+
+ def __eq__(self, other):
+ return self.value == other
+
+# def __delattr__(self, name):
+# delattr(self.value, name)
+#
+# def __getattr__(self, name):
+# return getattr(self.value, name)
+#
+# def __setattr__(self, name, value):
+# setattr(self.value, name, value)
+
+ def __delitem__(self, key):
+ del self.value[name]
+
+ def __getitem__(self, key):
+ return self.value[name]
+
+ def __setitem__(self, key, value):
+ self.value[name] = value
+
+
+try:
+ relpath = os.path.relpath
+except AttributeError:
+ def relpath(path, start='.'):
+ start_list = os.path.abspath(start).split(os.sep)
+ path_list = os.path.abspath(path).split(os.sep)
+
+ # Work out how much of the filepath is shared by start and path.
+ i = len(os.path.commonprefix([start_list, path_list]))
+
+ rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:]
+ return os.path.join(*rel_list)
diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py
new file mode 100755
index 0000000..a13de62
--- /dev/null
+++ b/scripts/import_cldr.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+import copy
+from optparse import OptionParser
+import os
+import pickle
+import sys
+try:
+ from xml.etree.ElementTree import parse
+except ImportError:
+ from elementtree.ElementTree import parse
+
+from babel.dates import parse_pattern
+
+def _parent(locale):
+ parts = locale.split('_')
+ if len(parts) == 1:
+ return 'root'
+ else:
+ return '_'.join(parts[:-1])
+
+def _text(elem):
+ buf = [elem.text or '']
+ for child in elem:
+ buf.append(_text(child))
+ buf.append(elem.tail or '')
+ return u''.join(filter(None, buf)).strip()
+
+def main():
+ parser = OptionParser(usage='%prog path/to/cldr')
+ options, args = parser.parse_args()
+ if len(args) != 1:
+ parser.error('incorrect number of arguments')
+
+ srcdir = args[0]
+ destdir = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])),
+ '..', 'babel', 'localedata')
+
+ filenames = os.listdir(os.path.join(srcdir, 'main'))
+ filenames.remove('root.xml')
+ filenames.sort(lambda a,b: len(a)-len(b))
+ filenames.insert(0, 'root.xml')
+
+ dicts = {}
+
+ for filename in filenames:
+ print>>sys.stderr, 'Processing input file %r' % filename
+ stem, ext = os.path.splitext(filename)
+ if ext != '.xml':
+ continue
+
+ data = {}
+ if stem != 'root':
+ data.update(copy.deepcopy(dicts[_parent(stem)]))
+ tree = parse(os.path.join(srcdir, 'main', filename))
+
+ # <localeDisplayNames>
+
+ territories = data.setdefault('territories', {})
+ for elem in tree.findall('//territories/territory'):
+ if 'draft' in elem.attrib and elem.attrib['type'] in territories:
+ continue
+ territories[elem.attrib['type']] = _text(elem)
+
+ languages = data.setdefault('languages', {})
+ for elem in tree.findall('//languages/language'):
+ if 'draft' in elem.attrib and elem.attrib['type'] in languages:
+ continue
+ languages[elem.attrib['type']] = _text(elem)
+
+ variants = data.setdefault('variants', {})
+ for elem in tree.findall('//variants/variant'):
+ if 'draft' in elem.attrib and elem.attrib['type'] in variants:
+ continue
+ variants[elem.attrib['type']] = _text(elem)
+
+ scripts = data.setdefault('scripts', {})
+ for elem in tree.findall('//scripts/script'):
+ if 'draft' in elem.attrib and elem.attrib['type'] in scripts:
+ continue
+ scripts[elem.attrib['type']] = _text(elem)
+
+ # <dates>
+
+ time_zones = data.setdefault('time_zones', {})
+ for elem in tree.findall('//timeZoneNames/zone'):
+ time_zones[elem.tag] = unicode(elem.findtext('displayName'))
+
+ for calendar in tree.findall('//calendars/calendar'):
+ if calendar.attrib['type'] != 'gregorian':
+ # TODO: support other calendar types
+ continue
+
+ months = data.setdefault('months', {})
+ for ctxt in calendar.findall('months/monthContext'):
+ ctxts = months.setdefault(ctxt.attrib['type'], {})
+ for width in ctxt.findall('monthWidth'):
+ widths = ctxts.setdefault(width.attrib['type'], {})
+ for elem in width.findall('month'):
+ if 'draft' in elem.attrib and int(elem.attrib['type']) in widths:
+ continue
+ widths[int(elem.attrib.get('type'))] = unicode(elem.text)
+
+ days = data.setdefault('days', {})
+ for ctxt in calendar.findall('days/dayContext'):
+ ctxts = days.setdefault(ctxt.attrib['type'], {})
+ for width in ctxt.findall('dayWidth'):
+ widths = ctxts.setdefault(width.attrib['type'], {})
+ for elem in width.findall('day'):
+ dtype = {'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4,
+ 'fri': 5, 'sat': 6, 'sun': 7}[elem.attrib['type']]
+ if 'draft' in elem.attrib and dtype in widths:
+ continue
+ widths[dtype] = unicode(elem.text)
+
+ quarters = data.setdefault('quarters', {})
+ for ctxt in calendar.findall('quarters/quarterContext'):
+ ctxts = quarters.setdefault(ctxt.attrib['type'], {})
+ for width in ctxt.findall('quarterWidth'):
+ widths = ctxts.setdefault(width.attrib['type'], {})
+ for elem in width.findall('quarter'):
+ if 'draft' in elem.attrib and int(elem.attrib['type']) in widths:
+ continue
+ widths[int(elem.attrib.get('type'))] = unicode(elem.text)
+
+ eras = data.setdefault('eras', {})
+ for width in calendar.findall('eras/*'):
+ ewidth = {'eraNames': 'wide', 'eraAbbr': 'abbreviated'}[width.tag]
+ widths = eras.setdefault(ewidth, {})
+ for elem in width.findall('era'):
+ if 'draft' in elem.attrib and int(elem.attrib['type']) in widths:
+ continue
+ widths[int(elem.attrib.get('type'))] = unicode(elem.text)
+
+ # AM/PM
+ periods = data.setdefault('periods', {})
+ for elem in calendar.findall('am'):
+ if 'draft' in elem.attrib and elem.tag in periods:
+ continue
+ periods[elem.tag] = unicode(elem.text)
+ for elem in calendar.findall('pm'):
+ if 'draft' in elem.attrib and elem.tag in periods:
+ continue
+ periods[elem.tag] = unicode(elem.text)
+
+ date_formats = data.setdefault('date_formats', {})
+ for elem in calendar.findall('dateFormats/dateFormatLength'):
+ if 'draft' in elem.attrib and elem.attrib.get('type') in date_formats:
+ continue
+ try:
+ date_formats[elem.attrib.get('type')] = \
+ parse_pattern(unicode(elem.findtext('dateFormat/pattern')))
+ except ValueError, e:
+ print e
+
+ time_formats = data.setdefault('time_formats', {})
+ for elem in calendar.findall('timeFormats/timeFormatLength'):
+ if 'draft' in elem.attrib and elem.attrib.get('type') in time_formats:
+ continue
+ try:
+ time_formats[elem.attrib.get('type')] = \
+ parse_pattern(unicode(elem.findtext('timeFormat/pattern')))
+ except ValueError, e:
+ print e
+
+ # <numbers>
+
+ number_symbols = data.setdefault('number_symbols', {})
+ for elem in tree.findall('//numbers/symbols/*'):
+ number_symbols[elem.tag] = unicode(elem.text)
+
+ decimal_formats = data.setdefault('decimal_formats', {})
+ for elem in tree.findall('//decimalFormats/decimalFormatLength'):
+ if 'draft' in elem.attrib and elem.attrib.get('type') in decimal_formats:
+ continue
+ decimal_formats[elem.attrib.get('type')] = unicode(elem.findtext('decimalFormat/pattern'))
+
+ scientific_formats = data.setdefault('scientific_formats', {})
+ for elem in tree.findall('//scientificFormats/scientificFormatLength'):
+ if 'draft' in elem.attrib and elem.attrib.get('type') in scientific_formats:
+ continue
+ scientific_formats[elem.attrib.get('type')] = unicode(elem.findtext('scientificFormat/pattern'))
+
+ currency_formats = data.setdefault('currency_formats', {})
+ for elem in tree.findall('//currencyFormats/currencyFormatLength'):
+ if 'draft' in elem.attrib and elem.attrib.get('type') in currency_formats:
+ continue
+ currency_formats[elem.attrib.get('type')] = unicode(elem.findtext('currencyFormat/pattern'))
+
+ percent_formats = data.setdefault('percent_formats', {})
+ for elem in tree.findall('//percentFormats/percentFormatLength'):
+ if 'draft' in elem.attrib and elem.attrib.get('type') in percent_formats:
+ continue
+ percent_formats[elem.attrib.get('type')] = unicode(elem.findtext('percentFormat/pattern'))
+
+ currencies = data.setdefault('currencies', {})
+ for elem in tree.findall('//currencies/currency'):
+ currencies[elem.attrib['type']] = {
+ 'display_name': unicode(elem.findtext('displayName')),
+ 'symbol': unicode(elem.findtext('symbol'))
+ }
+
+ dicts[stem] = data
+ outfile = open(os.path.join(destdir, stem + '.dat'), 'wb')
+ try:
+ pickle.dump(data, outfile, 2)
+ finally:
+ outfile.close()
+
+if __name__ == '__main__':
+ main()
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..8ad82af
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://babel.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://babel.edgewall.org/log/.
+
+import doctest
+from glob import glob
+import os
+from setuptools import find_packages, setup, Command
+import sys
+
+
+class build_doc(Command):
+ description = 'Builds the documentation'
+ user_options = []
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ from docutils.core import publish_cmdline
+ docutils_conf = os.path.join('doc', 'docutils.conf')
+ epydoc_conf = os.path.join('doc', 'epydoc.conf')
+
+ for source in glob('doc/*.txt'):
+ dest = os.path.splitext(source)[0] + '.html'
+ if not os.path.exists(dest) or \
+ os.path.getmtime(dest) < os.path.getmtime(source):
+ print 'building documentation file %s' % dest
+ publish_cmdline(writer_name='html',
+ argv=['--config=%s' % docutils_conf, source,
+ dest])
+
+ try:
+ from epydoc import cli
+ old_argv = sys.argv[1:]
+ sys.argv[1:] = [
+ '--config=%s' % epydoc_conf,
+ '--no-private', # epydoc bug, not read from config
+ '--simple-term',
+ '--verbose'
+ ]
+ cli.cli()
+ sys.argv[1:] = old_argv
+
+ except ImportError:
+ print 'epydoc not installed, skipping API documentation.'
+
+
+class test_doc(Command):
+ description = 'Tests the code examples in the documentation'
+ user_options = []
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ for filename in glob('doc/*.txt'):
+ print 'testing documentation file %s' % filename
+ doctest.testfile(filename, False, optionflags=doctest.ELLIPSIS)
+
+
+setup(
+ name = 'Babel',
+ version = '0.1',
+ description = 'Internationalization utilities',
+ long_description = \
+"""A collection of tools for internationalizing Python applications.""",
+ author = 'Edgewall Software',
+ author_email = 'info@edgewall.org',
+ license = 'BSD',
+ url = 'http://babel.edgewall.org/',
+ download_url = 'http://babel.edgewall.org/wiki/Download',
+ zip_safe = False,
+
+ classifiers = [
+ 'Development Status :: 4 - Beta',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ ],
+ packages = find_packages(exclude=['tests']),
+ package_data = {'babel': ['localedata/*.dat']},
+ test_suite = 'babel.tests.suite',
+
+ entry_points = """
+ [console_scripts]
+ pygettext = babel.catalog.frontend:main
+
+ [distutils.commands]
+ extract_messages = babel.catalog.frontend:extract_messages
+
+ [babel.extractors]
+ genshi = babel.catalog.extract:extract_genshi
+ python = babel.catalog.extract:extract_python
+ """,
+
+ cmdclass = {'build_doc': build_doc, 'test_doc': test_doc}
+)