Mercurial > babel > old > mirror
changeset 142:4a7af44e6695 stable
Create branch for 0.8.x releases.
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/0.8.x/COPYING @@ -0,0 +1,28 @@ +Copyright (C) 2007 Edgewall Software +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. The name of the author may not be used to endorse or promote + products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
new file mode 100644 --- /dev/null +++ b/0.8.x/ChangeLog @@ -0,0 +1,5 @@ +Version 0.8 +http://svn.edgewall.org/repos/babel/tags/0.8.0/ +(Jun 20 2007, from branches/stable/0.8.x) + + * First public release
new file mode 100644 --- /dev/null +++ b/0.8.x/INSTALL.txt @@ -0,0 +1,38 @@ +Installing Babel +================ + +Prerequisites +------------- + + * Python 2.3 or later (2.4 or later is recommended) + * Optional: setuptools 0.6b1 or later + * Optional: pytz (strongly recommended for real time-zone support) + + +Installation +------------ + +Once you've downloaded and unpacked a Babel source release, enter the +directory where the archive was unpacked, and run: + + $ python setup.py install + +Note that you may need administrator/root privileges for this step, as +this command will by default attempt to install Babel to the Python +site-packages directory on your system. + +For advanced options, please refer to the easy_install and/or the distutils +documentation: + + http://peak.telecommunity.com/DevCenter/EasyInstall + http://docs.python.org/inst/inst.html + + +Support +------- + +If you encounter any problems with Babel, please don't hesitate to ask +questions on the Babel mailing list or IRC channel: + + http://babel.edgewall.org/wiki/MailingList + http://babel.edgewall.org/wiki/IrcChannel
new file mode 100644 --- /dev/null +++ b/0.8.x/MANIFEST.in @@ -0,0 +1,3 @@ +include babel/localedata/*.dat +include doc/api/*.* +include doc/*.html
new file mode 100644 --- /dev/null +++ b/0.8.x/README.txt @@ -0,0 +1,12 @@ +About Babel +=========== + +Babel is a Python library that provides an integrated collection of +utilities that assist with internationalizing and localizing Python +applications (in particular web-based applications.) + +Details can be found in the HTML files in the `doc` folder. + +For more information please visit the Babel web site: + + <http://babel.edgewall.org/>
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/__init__.py @@ -0,0 +1,35 @@ +# -*- 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/. + +"""Integrated collection of utilities that assist in internationalizing and +localizing applications. + +This package is basically composed of two major parts: + + * tools to build and work with ``gettext`` message catalogs + * a Python interface to the CLDR (Common Locale Data Repository), providing + access to various locale display names, localized number and date + formatting, etc. + +:see: http://www.gnu.org/software/gettext/ +:see: http://docs.python.org/lib/module-gettext.html +:see: http://www.unicode.org/cldr/ +""" + +from babel.core import * + +__docformat__ = 'restructuredtext en' +try: + __version__ = __import__('pkg_resources').get_distribution('Babel').version +except ImportError: + pass
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/core.py @@ -0,0 +1,604 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Core locale representation and locale data access.""" + +import os + +from babel import localedata + +__all__ = ['UnknownLocaleError', 'Locale', 'default_locale', 'negotiate_locale', + 'parse_locale'] +__docformat__ = 'restructuredtext en' + + +class UnknownLocaleError(Exception): + """Exception thrown when a locale is requested for which no locale data + is available. + """ + + def __init__(self, identifier): + """Create the exception. + + :param identifier: the identifier string of the unsupported locale + """ + Exception.__init__(self, 'unknown locale %r' % identifier) + self.identifier = identifier + + +class Locale(object): + """Representation of a specific locale. + + >>> locale = Locale('en', 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'.' + + If a locale is requested for which no locale data is available, an + `UnknownLocaleError` is raised: + + >>> Locale.parse('en_DE') + Traceback (most recent call last): + ... + UnknownLocaleError: unknown locale 'en_DE' + + :see: `IETF RFC 3066 <http://www.ietf.org/rfc/rfc3066.txt>`_ + """ + + def __init__(self, language, territory=None, 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 + :raise `UnknownLocaleError`: if no locale data is available for the + requested locale + """ + self.language = language + self.territory = territory + self.variant = variant + self.__data = None + + identifier = str(self) + if not localedata.exists(identifier): + raise UnknownLocaleError(identifier) + + def default(cls, category=None): + """Return the system default locale for the specified category. + + >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']: + ... os.environ[name] = '' + >>> os.environ['LANG'] = 'fr_FR.UTF-8' + >>> Locale.default('LC_MESSAGES') + <Locale "fr_FR"> + + :param category: one of the ``LC_XXX`` environment variable names + :return: the value of the variable, or any of the fallbacks + (``LANGUAGE``, ``LC_ALL``, ``LC_CTYPE``, and ``LANG``) + :rtype: `Locale` + """ + return cls(default_locale(category)) + default = classmethod(default) + + def negotiate(cls, preferred, available, sep='_'): + """Find the best match between available and requested locale strings. + + >>> Locale.negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT']) + <Locale "de_DE"> + >>> Locale.negotiate(['de_DE', 'en_US'], ['en', 'de']) + <Locale "de"> + >>> Locale.negotiate(['de_DE', 'de'], ['en_US']) + + :param preferred: the list of locale identifers preferred by the user + :param available: the list of locale identifiers available + :return: the `Locale` object for the best match, or `None` if no match + was found + :rtype: `Locale` + """ + identifier = negotiate_locale(preferred, available, sep=sep) + if identifier: + return Locale.parse(identifier) + negotiate = classmethod(negotiate) + + def parse(cls, identifier, sep='_'): + """Create a `Locale` instance for the given locale identifier. + + >>> l = Locale.parse('de-DE', sep='-') + >>> l.display_name + u'Deutsch (Deutschland)' + + If the `identifier` parameter is not a string, but actually a `Locale` + object, that object is returned: + + >>> Locale.parse(l) + <Locale "de_DE"> + + :param identifier: the locale identifier string + :param sep: optional component separator + :return: a corresponding `Locale` instance + :rtype: `Locale` + :raise `ValueError`: if the string does not appear to be a valid locale + identifier + :raise `UnknownLocaleError`: if no locale data is available for the + requested locale + """ + if type(identifier) is cls: + return identifier + return cls(*parse_locale(identifier, sep=sep)) + parse = classmethod(parse) + + def __eq__(self, other): + return str(self) == str(other) + + 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: + self.__data = localedata.load(str(self)) + 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)' + >>> Locale('sv').display_name + u'svenska' + + :type: `unicode` + """) + + def english_name(self): + en = Locale('en') + retval = en.languages.get(self.language) + if self.territory: + variant = '' + if self.variant: + variant = ', %s' % en.variants.get(self.variant) + retval += ' (%s%s)' % (en.territories.get(self.territory), + variant) + return retval + english_name = property(english_name, doc="""\ + The english display name of the locale. + + >>> Locale('de').english_name + u'German' + >>> Locale('de', 'DE').english_name + u'German (Germany)' + + :type: `unicode` + """) + + #{ General Locale Display Names + + def languages(self): + return self._data['languages'] + languages = property(languages, doc="""\ + Mapping of language codes to translated language names. + + >>> Locale('de', 'DE').languages['ja'] + u'Japanisch' + + :type: `dict` + :see: `ISO 639 <http://www.loc.gov/standards/iso639-2/>`_ + """) + + def scripts(self): + return self._data['scripts'] + scripts = property(scripts, doc="""\ + Mapping of script codes to translated script names. + + >>> Locale('en', 'US').scripts['Hira'] + u'Hiragana' + + :type: `dict` + :see: `ISO 15924 <http://www.evertype.com/standards/iso15924/>`_ + """) + + def territories(self): + return self._data['territories'] + territories = property(territories, doc="""\ + Mapping of script codes to translated script names. + + >>> Locale('es', 'CO').territories['DE'] + u'Alemania' + + :type: `dict` + :see: `ISO 3166 <http://www.iso.org/iso/en/prods-services/iso3166ma/>`_ + """) + + def variants(self): + return self._data['variants'] + variants = property(variants, doc="""\ + Mapping of script codes to translated script names. + + >>> Locale('de', 'DE').variants['1901'] + u'alte deutsche Rechtschreibung' + + :type: `dict` + """) + + #{ Number Formatting + + def currencies(self): + return self._data['currency_names'] + currencies = property(currencies, doc="""\ + Mapping of currency codes to translated currency names. + + >>> Locale('en').currencies['COP'] + u'Colombian Peso' + >>> Locale('de', 'DE').currencies['COP'] + u'Kolumbianischer Peso' + + :type: `dict` + """) + + def currency_symbols(self): + return self._data['currency_symbols'] + currency_symbols = property(currency_symbols, doc="""\ + Mapping of currency codes to symbols. + + >>> Locale('en').currency_symbols['USD'] + u'US$' + >>> Locale('en', 'US').currency_symbols['USD'] + u'$' + + :type: `dict` + """) + + def number_symbols(self): + return self._data['number_symbols'] + number_symbols = property(number_symbols, doc="""\ + Symbols used in number formatting. + + >>> Locale('fr', 'FR').number_symbols['decimal'] + u',' + + :type: `dict` + """) + + def decimal_formats(self): + return self._data['decimal_formats'] + decimal_formats = property(decimal_formats, doc="""\ + Locale patterns for decimal number formatting. + + >>> Locale('en', 'US').decimal_formats[None] + <NumberPattern u'#,##0.###'> + + :type: `dict` + """) + + def currency_formats(self): + return self._data['currency_formats'] + currency_formats = property(currency_formats, doc=r"""\ + Locale patterns for currency number formatting. + + >>> print Locale('en', 'US').currency_formats[None] + <NumberPattern u'\xa4#,##0.00'> + + :type: `dict` + """) + + def percent_formats(self): + return self._data['percent_formats'] + percent_formats = property(percent_formats, doc="""\ + Locale patterns for percent number formatting. + + >>> Locale('en', 'US').percent_formats[None] + <NumberPattern u'#,##0%'> + + :type: `dict` + """) + + def scientific_formats(self): + return self._data['scientific_formats'] + scientific_formats = property(scientific_formats, doc="""\ + Locale patterns for scientific number formatting. + + >>> Locale('en', 'US').scientific_formats[None] + <NumberPattern u'#E0'> + + :type: `dict` + """) + + #{ Calendar Information and Date Formatting + + def periods(self): + return self._data['periods'] + periods = property(periods, doc="""\ + Locale display names for day periods (AM/PM). + + >>> Locale('en', 'US').periods['am'] + u'AM' + + :type: `dict` + """) + + def days(self): + return self._data['days'] + days = property(days, doc="""\ + Locale display names for weekdays. + + >>> Locale('de', 'DE').days['format']['wide'][3] + u'Donnerstag' + + :type: `dict` + """) + + def months(self): + return self._data['months'] + months = property(months, doc="""\ + Locale display names for months. + + >>> Locale('de', 'DE').months['format']['wide'][10] + u'Oktober' + + :type: `dict` + """) + + def quarters(self): + return self._data['quarters'] + quarters = property(quarters, doc="""\ + Locale display names for quarters. + + >>> Locale('de', 'DE').quarters['format']['wide'][1] + u'1. Quartal' + + :type: `dict` + """) + + def eras(self): + return self._data['eras'] + eras = property(eras, doc="""\ + Locale display names for eras. + + >>> Locale('en', 'US').eras['wide'][1] + u'Anno Domini' + >>> Locale('en', 'US').eras['abbreviated'][0] + u'BC' + + :type: `dict` + """) + + def time_zones(self): + return self._data['time_zones'] + time_zones = property(time_zones, doc="""\ + Locale display names for time zones. + + >>> Locale('en', 'US').time_zones['America/Los_Angeles']['long']['standard'] + u'Pacific Standard Time' + >>> Locale('en', 'US').time_zones['Europe/Dublin']['city'] + u'Dublin' + + :type: `dict` + """) + + def zone_aliases(self): + return self._data['zone_aliases'] + zone_aliases = property(zone_aliases, doc="""\ + Mapping of time zone aliases to their respective canonical identifer. + + >>> Locale('en').zone_aliases['UTC'] + 'Etc/GMT' + + :type: `dict` + :note: this doesn't really belong here, as it does not change between + locales + """) + + def first_week_day(self): + return self._data['week_data']['first_day'] + first_week_day = property(first_week_day, doc="""\ + The first day of a week. + + >>> Locale('de', 'DE').first_week_day + 0 + >>> Locale('en', 'US').first_week_day + 6 + + :type: `int` + """) + + def weekend_start(self): + return self._data['week_data']['weekend_start'] + weekend_start = property(weekend_start, doc="""\ + The day the weekend starts. + + >>> Locale('de', 'DE').weekend_start + 5 + + :type: `int` + """) + + def weekend_end(self): + return self._data['week_data']['weekend_end'] + weekend_end = property(weekend_end, doc="""\ + The day the weekend ends. + + >>> Locale('de', 'DE').weekend_end + 6 + + :type: `int` + """) + + def min_week_days(self): + return self._data['week_data']['min_days'] + min_week_days = property(min_week_days, doc="""\ + The minimum number of days in a week so that the week is counted as the + first week of a year or month. + + >>> Locale('de', 'DE').min_week_days + 4 + + :type: `int` + """) + + def date_formats(self): + return self._data['date_formats'] + date_formats = property(date_formats, doc="""\ + Locale patterns for date formatting. + + >>> Locale('en', 'US').date_formats['short'] + <DateTimePattern u'M/d/yy'> + >>> Locale('fr', 'FR').date_formats['long'] + <DateTimePattern u'd MMMM 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'] + <DateTimePattern u'h:mm a'> + >>> Locale('fr', 'FR').time_formats['long'] + <DateTimePattern u'HH:mm:ss z'> + + :type: `dict` + """) + + def datetime_formats(self): + return self._data['datetime_formats'] + datetime_formats = property(datetime_formats, doc="""\ + Locale patterns for datetime formatting. + + >>> Locale('en').datetime_formats[None] + u'{1} {0}' + >>> Locale('th').datetime_formats[None] + u'{1}, {0}' + + :type: `dict` + """) + + +def default_locale(category=None): + """Returns the system default locale for a given category, based on + environment variables. + + >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']: + ... os.environ[name] = '' + >>> os.environ['LANG'] = 'fr_FR.UTF-8' + >>> default_locale('LC_MESSAGES') + 'fr_FR' + + :param category: one of the ``LC_XXX`` environment variable names + :return: the value of the variable, or any of the fallbacks (``LANGUAGE``, + ``LC_ALL``, ``LC_CTYPE``, and ``LANG``) + + :rtype: `str` + """ + varnames = (category, 'LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG') + for name in filter(None, varnames): + locale = os.getenv(name) + if locale: + return '_'.join(filter(None, parse_locale(locale))) + +def negotiate_locale(preferred, available, sep='_'): + """Find the best match between available and requested locale strings. + + >>> negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT']) + 'de_DE' + >>> negotiate_locale(['de_DE', 'en_US'], ['en', 'de']) + 'de' + + :param preferred: the list of locale strings preferred by the user + :param available: the list of locale strings available + :param sep: character that separates the different parts of the locale + strings + :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(sep) + if len(parts) > 1 and parts[0] in available: + return parts[0] + return None + +def parse_locale(identifier, sep='_'): + """Parse a locale identifier into a ``(language, territory, variant)`` + tuple. + + >>> parse_locale('zh_CN') + ('zh', 'CN', None) + + The default component separator is "_", but a different separator can be + specified using the `sep` parameter: + + >>> parse_locale('zh-CN', sep='-') + ('zh', 'CN', None) + + :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>`_ + """ + if '.' in identifier: + # this is probably the charset/encoding, which we don't care about + identifier = identifier.split('.', 1)[0] + 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
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/dates.py @@ -0,0 +1,643 @@ +# -*- 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, timedelta, tzinfo +import re + +from babel.core import default_locale, Locale +from babel.util import UTC + +__all__ = ['format_date', 'format_datetime', 'format_time', 'parse_date', + 'parse_datetime', 'parse_time'] +__docformat__ = 'restructuredtext en' + +LC_TIME = default_locale('LC_TIME') + +# Aliases for use in scopes where the modules are shadowed by local variables +date_ = date +datetime_ = datetime +time_ = time + +def get_period_names(locale=LC_TIME): + """Return the names for day periods (AM/PM) used by the locale. + + >>> get_period_names(locale='en_US')['am'] + u'AM' + + :param locale: the `Locale` object, or a locale string + :return: the dictionary of period names + :rtype: `dict` + """ + return Locale.parse(locale).periods + +def get_day_names(width='wide', context='format', locale=LC_TIME): + """Return the day names used by the locale for the specified format. + + >>> get_day_names('wide', locale='en_US')[1] + u'Tuesday' + >>> get_day_names('abbreviated', locale='es')[1] + u'mar' + >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1] + u'D' + + :param width: the width to use, one of "wide", "abbreviated", or "narrow" + :param context: the context, either "format" or "stand-alone" + :param locale: the `Locale` object, or a locale string + :return: the dictionary of day names + :rtype: `dict` + """ + return Locale.parse(locale).days[context][width] + +def get_month_names(width='wide', context='format', locale=LC_TIME): + """Return the month names used by the locale for the specified format. + + >>> get_month_names('wide', locale='en_US')[1] + u'January' + >>> get_month_names('abbreviated', locale='es')[1] + u'ene' + >>> get_month_names('narrow', context='stand-alone', locale='de_DE')[1] + u'J' + + :param width: the width to use, one of "wide", "abbreviated", or "narrow" + :param context: the context, either "format" or "stand-alone" + :param locale: the `Locale` object, or a locale string + :return: the dictionary of month names + :rtype: `dict` + """ + return Locale.parse(locale).months[context][width] + +def get_quarter_names(width='wide', context='format', locale=LC_TIME): + """Return the quarter names used by the locale for the specified format. + + >>> get_quarter_names('wide', locale='en_US')[1] + u'1st quarter' + >>> get_quarter_names('abbreviated', locale='de_DE')[1] + u'Q1' + + :param width: the width to use, one of "wide", "abbreviated", or "narrow" + :param context: the context, either "format" or "stand-alone" + :param locale: the `Locale` object, or a locale string + :return: the dictionary of quarter names + :rtype: `dict` + """ + return Locale.parse(locale).quarters[context][width] + +def get_era_names(width='wide', locale=LC_TIME): + """Return the era names used by the locale for the specified format. + + >>> get_era_names('wide', locale='en_US')[1] + u'Anno Domini' + >>> get_era_names('abbreviated', locale='de_DE')[1] + u'n. Chr.' + + :param width: the width to use, either "wide" 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') + <DateTimePattern u'MMM d, yyyy'> + >>> get_date_format('full', locale='de_DE') + <DateTimePattern 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: `DateTimePattern` + """ + return Locale.parse(locale).date_formats[format] + +def get_datetime_format(format='medium', locale=LC_TIME): + """Return the datetime formatting patterns used by the locale for the + specified format. + + >>> get_datetime_format(locale='en_US') + u'{1} {0}' + + :param format: the format to use, one of "full", "long", "medium", or + "short" + :param locale: the `Locale` object, or a locale string + :return: the datetime format pattern + :rtype: `unicode` + """ + patterns = Locale.parse(locale).datetime_formats + if format not in patterns: + format = None + return patterns[format] + +def get_time_format(format='medium', locale=LC_TIME): + """Return the time formatting patterns used by the locale for the specified + format. + + >>> get_time_format(locale='en_US') + <DateTimePattern u'h:mm:ss a'> + >>> get_time_format('full', locale='de_DE') + <DateTimePattern u"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: `DateTimePattern` + """ + return Locale.parse(locale).time_formats[format] + +def format_date(date=None, format='medium', locale=LC_TIME): + """Return a date formatted according to the given pattern. + + >>> d = date(2007, 04, 01) + >>> format_date(d, locale='en_US') + u'Apr 1, 2007' + >>> format_date(d, format='full', locale='de_DE') + u'Sonntag, 1. April 2007' + + If you don't want to use the locale default formats, you can specify a + custom date pattern: + + >>> format_date(d, "EEE, MMM d, ''yy", locale='en') + u"Sun, Apr 1, '07" + + :param date: the ``date`` or ``datetime`` object; if `None`, the current + date is used + :param format: one of "full", "long", "medium", or "short", or a custom + date/time pattern + :param locale: a `Locale` object or a locale identifier + :rtype: `unicode` + + :note: If the pattern contains time fields, an `AttributeError` will be + raised when trying to apply the formatting. This is also true if + the value of ``date`` parameter is actually a ``datetime`` object, + as this function automatically converts that to a ``date``. + """ + if date is None: + date = date_.today() + elif isinstance(date, datetime): + date = date.date() + + locale = Locale.parse(locale) + if format in ('full', 'long', 'medium', 'short'): + format = get_date_format(format, locale=locale) + pattern = parse_pattern(format) + return parse_pattern(format).apply(date, locale) + +def format_datetime(datetime=None, format='medium', tzinfo=None, + locale=LC_TIME): + """Return a date formatted according to the given pattern. + + >>> dt = datetime(2007, 04, 01, 15, 30) + >>> format_datetime(dt, locale='en_US') + u'Apr 1, 2007 3:30:00 PM' + + For any pattern requiring the display of the time-zone, the third-party + ``pytz`` package is needed to explicitly specify the time-zone: + + >>> from pytz import timezone + >>> format_datetime(dt, 'full', tzinfo=timezone('Europe/Berlin'), + ... locale='de_DE') + u'Sonntag, 1. April 2007 17:30 Uhr MESZ' + >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz", + ... tzinfo=timezone('US/Eastern'), locale='en') + u'2007.04.01 AD at 11:30:00 EDT' + + :param datetime: the `datetime` object; if `None`, the current date and + time is used + :param format: one of "full", "long", "medium", or "short", or a custom + date/time pattern + :param tzinfo: the timezone to apply to the time for display + :param locale: a `Locale` object or a locale identifier + :rtype: `unicode` + """ + if datetime is None: + datetime = datetime_.now() + elif isinstance(datetime, (int, long)): + datetime = datetime.fromtimestamp(datetime) + elif isinstance(datetime, time): + datetime = datetime_.combine(date.today(), datetime) + if datetime.tzinfo is None: + datetime = datetime.replace(tzinfo=UTC) + if tzinfo is not None: + datetime = datetime.astimezone(tzinfo) + if hasattr(tzinfo, 'normalize'): # pytz + datetime = tzinfo.normalize(datetime) + + locale = Locale.parse(locale) + if format in ('full', 'long', 'medium', 'short'): + return get_datetime_format(format, locale=locale) \ + .replace('{0}', format_time(datetime, format, tzinfo=None, + locale=locale)) \ + .replace('{1}', format_date(datetime, format, locale=locale)) + else: + return parse_pattern(format).apply(datetime, locale) + +def format_time(time=None, format='medium', tzinfo=None, locale=LC_TIME): + """Return a time formatted according to the given pattern. + + >>> t = time(15, 30) + >>> format_time(t, locale='en_US') + u'3:30:00 PM' + >>> format_time(t, format='short', locale='de_DE') + u'15:30' + + If you don't want to use the locale default formats, you can specify a + custom time pattern: + + >>> format_time(t, "hh 'o''clock' a", locale='en') + u"03 o'clock PM" + + For any pattern requiring the display of the time-zone, the third-party + ``pytz`` package is needed to explicitly specify the time-zone: + + >>> from pytz import timezone + >>> t = time(15, 30) + >>> format_time(t, format='full', tzinfo=timezone('Europe/Berlin'), + ... locale='de_DE') + u'17:30 Uhr MESZ' + >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=timezone('US/Eastern'), + ... locale='en') + u"11 o'clock AM, Eastern Daylight Time" + + :param time: the ``time`` or ``datetime`` object; if `None`, the current + time is used + :param format: one of "full", "long", "medium", or "short", or a custom + date/time pattern + :param tzinfo: the time-zone to apply to the time for display + :param locale: a `Locale` object or a locale identifier + :rtype: `unicode` + + :note: If the pattern contains date fields, an `AttributeError` will be + raised when trying to apply the formatting. This is also true if + the value of ``time`` parameter is actually a ``datetime`` object, + as this function automatically converts that to a ``time``. + """ + if time is None: + time = datetime.now().time() + elif isinstance(time, (int, long)): + time = datetime.fromtimestamp(time).time() + elif isinstance(time, datetime): + time = time.timetz() + if time.tzinfo is None: + time = time.replace(tzinfo=UTC) + if tzinfo is not None: + dt = datetime.combine(date.today(), time).astimezone(tzinfo) + if hasattr(tzinfo, 'normalize'): # pytz + dt = tzinfo.normalize(dt) + time = dt.timetz() + + 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): + """Parse a date from a string. + + This function uses the date format for the locale as a hint to determine + the order in which the date fields appear in the string. + + >>> parse_date('4/1/04', locale='en_US') + datetime.date(2004, 4, 1) + >>> parse_date('01.04.2004', locale='de_DE') + datetime.date(2004, 4, 1) + + :param string: the string containing the date + :param locale: a `Locale` object or a locale identifier + :return: the parsed date + :rtype: `date` + """ + # TODO: try ISO format first? + format = get_date_format(locale=locale).pattern.lower() + year_idx = format.index('y') + month_idx = format.index('m') + if month_idx < 0: + month_idx = format.index('l') + day_idx = format.index('d') + + indexes = [(year_idx, 'Y'), (month_idx, 'M'), (day_idx, 'D')] + indexes.sort() + indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)]) + + # FIXME: this currently only supports numbers, but should also support month + # names, both in the requested locale, and english + + numbers = re.findall('(\d+)', string) + year = numbers[indexes['Y']] + if len(year) == 2: + year = 2000 + int(year) + else: + year = int(year) + month = int(numbers[indexes['M']]) + day = int(numbers[indexes['D']]) + if month > 12: + month, day = day, month + return date(year, month, day) + +def parse_datetime(string, locale=LC_TIME): + """Parse a date and time from a string. + + This function uses the date and time formats for the locale as a hint to + determine the order in which the time fields appear in the string. + + :param string: the string containing the date and time + :param locale: a `Locale` object or a locale identifier + :return: the parsed date/time + :rtype: `datetime` + """ + raise NotImplementedError + +def parse_time(string, locale=LC_TIME): + """Parse a time from a string. + + This function uses the time format for the locale as a hint to determine + the order in which the time fields appear in the string. + + >>> parse_time('15:30:00', locale='en_US') + datetime.time(15, 30) + + :param string: the string containing the time + :param locale: a `Locale` object or a locale identifier + :return: the parsed time + :rtype: `time` + """ + # TODO: try ISO format first? + format = get_time_format(locale=locale).pattern.lower() + hour_idx = format.index('h') + if hour_idx < 0: + hour_idx = format.index('k') + min_idx = format.index('m') + sec_idx = format.index('s') + + indexes = [(hour_idx, 'H'), (min_idx, 'M'), (sec_idx, 'S')] + indexes.sort() + indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)]) + + # FIXME: support 12 hour clock, and 0-based hour specification + # and seconds should be optional, maybe minutes too + # oh, and time-zones, of course + + numbers = re.findall('(\d+)', string) + hour = int(numbers[indexes['H']]) + minute = int(numbers[indexes['M']]) + second = int(numbers[indexes['S']]) + return time(hour, minute, second) + + +class DateTimePattern(object): + + def __init__(self, pattern, format): + self.pattern = pattern + self.format = format + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.pattern) + + def __unicode__(self): + return self.pattern + + def __mod__(self, other): + 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)) + if isinstance(value, (datetime, time)) and value.tzinfo is None: + value = value.replace(tzinfo=UTC) + self.value = value + self.locale = Locale.parse(locale) + + def __getitem__(self, name): + # TODO: a number of fields missing here + char = name[0] + num = len(name) + if char == 'G': + return self.format_era(char, num) + elif char in ('y', 'Y'): + return self.format_year(char, num) + elif char in ('Q', 'q'): + return self.format_quarter(char, num) + elif char in ('M', 'L'): + return self.format_month(char, num) + elif char == 'd': + return self.format(self.value.day, num) + elif char in ('E', 'e', 'c'): + return self.format_weekday(char, num) + elif char == 'a': + return self.format_period(char) + elif char == 'h': + return self.format(self.value.hour % 12, num) + elif char == 'H': + return self.format(self.value.hour, num) + elif char == 'K': + return self.format(self.value.hour % 12 - 1, num) + elif char == 'k': + return self.format(self.value.hour + 1, num) + elif char == 'm': + return self.format(self.value.minute, num) + elif char == 's': + return self.format(self.value.second, num) + elif char in ('z', 'Z', 'v'): + return self.format_timezone(char, num) + else: + raise KeyError('Unsupported date/time field %r' % char) + + def format_era(self, char, num): + width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)] + era = int(self.value.year >= 0) + return get_era_names(width, self.locale)[era] + + def format_year(self, char, num): + if char.islower(): + value = self.value.year + else: + value = self.value.isocalendar()[0] + year = self.format(value, num) + if num == 2: + year = year[-2:] + return year + + def format_month(self, char, num): + if num <= 2: + return ('%%0%dd' % num) % self.value.month + width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] + context = {3: 'format', 4: 'format', 5: 'stand-alone'}[num] + return get_month_names(width, context, self.locale)[self.value.month] + + def format_weekday(self, char, num): + if num < 3: + if char.islower(): + value = 7 - self.locale.first_week_day + self.value.weekday() + return self.format(value % 7 + 1, num) + num = 3 + weekday = self.value.weekday() + width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] + context = {3: 'format', 4: 'format', 5: 'stand-alone'}[num] + return get_day_names(width, context, self.locale)[weekday] + + def format_period(self, char): + period = {0: 'am', 1: 'pm'}[int(self.value.hour > 12)] + return get_period_names(locale=self.locale)[period] + + def format_timezone(self, char, num): + if char in ('z', 'v'): + if hasattr(self.value.tzinfo, 'zone'): + zone = self.value.tzinfo.zone + else: + zone = self.value.tzinfo.tzname(self.value) + + # Get the canonical time-zone code + zone = self.locale.zone_aliases.get(zone, zone) + + # Try explicitly translated zone names first + display = self.locale.time_zones.get(zone) + if display: + if 'long' in display: + width = {3: 'short', 4: 'long'}[max(3, num)] + if char == 'v': + dst = 'generic' + else: + dst = self.value.dst() and 'daylight' or 'standard' + return display[width][dst] + elif 'city' in display: + return display['city'] + + else: + return zone.split('/', 1)[1] + + elif char == 'Z': + offset = self.value.utcoffset() + seconds = offset.days * 24 * 60 * 60 + offset.seconds + hours, seconds = divmod(seconds, 3600) + pattern = {3: '%+03d%02d', 4: 'GMT %+03d:%02d'}[max(3, num)] + return pattern % (hours, seconds // 60) + + + def format(self, value, length): + return ('%%0%dd' % length) % value + + +PATTERN_CHARS = { + 'G': [1, 2, 3, 4, 5], # era + 'y': None, 'Y': None, 'u': None, # year + 'Q': [1, 2, 3, 4], 'q': [1, 2, 3, 4], # quarter + 'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5], # month + 'w': [1, 2], 'W': [1], # week + 'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None, # day + 'E': [1, 2, 3, 4, 5], 'e': [1, 2, 3, 4, 5], 'c': [1, 3, 4, 5], # week day + 'a': [1], # period + 'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2], # hour + 'm': [1, 2], # minute + 's': [1, 2], 'S': None, 'A': None, # second + 'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4], 'v': [1, 4] # zone +} + +def parse_pattern(pattern): + """Parse date, time, and datetime format patterns. + + >>> parse_pattern("MMMMd").format + u'%(MMMM)s%(d)s' + >>> parse_pattern("MMM d, yyyy").format + u'%(MMM)s %(d)s, %(yyyy)s' + + Pattern can contain literal strings in single quotes: + + >>> parse_pattern("H:mm' Uhr 'z").format + u'%(H)s:%(mm)s Uhr %(z)s' + + An actual single quote can be used by using two adjacent single quote + characters: + + >>> parse_pattern("hh' o''clock'").format + u"%(hh)s o'clock" + + :param pattern: the formatting pattern to parse + """ + if type(pattern) is DateTimePattern: + return pattern + + result = [] + quotebuf = None + charbuf = [] + fieldchar = [''] + fieldnum = [0] + + def append_chars(): + result.append(''.join(charbuf).replace('%', '%%')) + del charbuf[:] + + def append_field(): + limit = PATTERN_CHARS[fieldchar[0]] + if limit and fieldnum[0] not in limit: + raise ValueError('Invalid length for field: %r' + % (fieldchar[0] * fieldnum[0])) + result.append('%%(%s)s' % (fieldchar[0] * fieldnum[0])) + fieldchar[0] = '' + fieldnum[0] = 0 + + for idx, char in enumerate(pattern.replace("''", '\0')): + if quotebuf is None: + if char == "'": # quote started + if fieldchar[0]: + append_field() + elif charbuf: + append_chars() + quotebuf = [] + elif char in PATTERN_CHARS: + if charbuf: + append_chars() + if char == fieldchar[0]: + fieldnum[0] += 1 + else: + if fieldchar[0]: + append_field() + fieldchar[0] = char + fieldnum[0] = 1 + else: + if fieldchar[0]: + append_field() + charbuf.append(char) + + elif quotebuf is not None: + if char == "'": # end of quote + charbuf.extend(quotebuf) + quotebuf = None + else: # inside quote + quotebuf.append(char) + + if fieldchar[0]: + append_field() + elif charbuf: + append_chars() + + return DateTimePattern(pattern, u''.join(result).replace('\0', "'"))
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/localedata.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Low-level locale data access. + +:note: The `Locale` class, which uses this module under the hood, provides a + more convenient interface for accessing the locale data. +""" + +import os +import pickle +try: + import threading +except ImportError: + import dummy_threading as threading + +__all__ = ['exists', 'load'] +__docformat__ = 'restructuredtext en' + +_cache = {} +_cache_lock = threading.RLock() +_dirname = os.path.join(os.path.dirname(__file__), 'localedata') + +def exists(name): + """Check whether locale data is available for the given locale. + + :param name: the locale identifier string + :return: `True` if the locale data exists, `False` otherwise + :rtype: `bool` + """ + if name in _cache: + return True + return os.path.exists(os.path.join(_dirname, '%s.dat' % name)) + +def load(name): + """Load the locale data for the given locale. + + The locale data is a dictionary that contains much of the data defined by + the Common Locale Data Repository (CLDR). This data is stored as a + collection of pickle files inside the ``babel`` package. + + >>> d = load('en_US') + >>> d['languages']['sv'] + u'Swedish' + + Note that the results are cached, and subsequent requests for the same + locale return the same dictionary: + + >>> d1 = load('en_US') + >>> d2 = load('en_US') + >>> d1 is d2 + True + + :param name: the locale identifier string (or "root") + :return: the locale data + :rtype: `dict` + :raise `IOError`: if no locale data file is found for the given locale + identifer, or one of the locales it inherits from + """ + _cache_lock.acquire() + try: + data = _cache.get(name) + if not data: + # Load inherited data + if name == 'root': + data = {} + else: + parts = name.split('_') + if len(parts) == 1: + parent = 'root' + else: + parent = '_'.join(parts[:-1]) + data = load(parent).copy() + filename = os.path.join(_dirname, '%s.dat' % name) + fileobj = open(filename, 'rb') + try: + if name != 'root': + merge(data, pickle.load(fileobj)) + else: + data = pickle.load(fileobj) + _cache[name] = data + finally: + fileobj.close() + return data + finally: + _cache_lock.release() + +def merge(dict1, dict2): + """Merge the data from `dict2` into the `dict1` dictionary, making copies + of nested dictionaries. + + :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: + if type(value) is dict: + dict1[key] = dict1.get(key, {}).copy() + merge(dict1[key], value) + else: + dict1[key] = value
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Support for ``gettext`` message catalogs.""" + +from babel.messages.catalog import *
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/catalog.py @@ -0,0 +1,476 @@ +# -*- 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/. + +"""Data structures for message catalogs.""" + +from datetime import datetime +from email import message_from_string +import re +try: + set +except NameError: + from sets import Set as set +import time + +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.util import odict, LOCALTZ, UTC, FixedOffsetTimezone + +__all__ = ['Message', 'Catalog'] +__docformat__ = 'restructuredtext en' + +PYTHON_FORMAT = re.compile(r'\%(\([\w]+\))?[diouxXeEfFgGcrs]').search + + +class Message(object): + """Representation of a single message in a catalog.""" + + def __init__(self, id, string='', locations=(), flags=(), auto_comments=(), + user_comments=()): + """Create the message object. + + :param id: the message ID, or a ``(singular, plural)`` tuple for + pluralizable messages + :param string: the translated message string, or a + ``(singular, plural)`` tuple for pluralizable messages + :param locations: a sequence of ``(filenname, lineno)`` tuples + :param flags: a set or sequence of flags + :param auto_comments: a sequence of automatic comments for the message + :param user_comments: a sequence of user comments for the message + """ + self.id = id #: The message ID + if not string and self.pluralizable: + string = (u'', u'') + self.string = string #: The message translation + self.locations = list(locations) + self.flags = set(flags) + if id and self.python_format: + self.flags.add('python-format') + else: + self.flags.discard('python-format') + self.auto_comments = list(auto_comments) + self.user_comments = list(user_comments) + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.id) + + def fuzzy(self): + return 'fuzzy' in self.flags + fuzzy = property(fuzzy, doc="""\ + Whether the translation is fuzzy. + + >>> Message('foo').fuzzy + False + >>> Message('foo', 'foo', flags=['fuzzy']).fuzzy + True + + :type: `bool` + """) + + def pluralizable(self): + return isinstance(self.id, (list, tuple)) + pluralizable = property(pluralizable, doc="""\ + Whether the message is plurizable. + + >>> Message('foo').pluralizable + False + >>> Message(('foo', 'bar')).pluralizable + True + + :type: `bool` + """) + + def python_format(self): + ids = self.id + if not isinstance(ids, (list, tuple)): + ids = [ids] + return bool(filter(None, [PYTHON_FORMAT(id) for id in ids])) + python_format = property(python_format, doc="""\ + Whether the message contains Python-style parameters. + + >>> Message('foo %(name)s bar').python_format + True + >>> Message(('foo %(name)s', 'foo %(name)s')).python_format + True + + :type: `bool` + """) + + +DEFAULT_HEADER = u"""\ +# Translations template for PROJECT. +# Copyright (C) YEAR ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +#""" + +class Catalog(object): + """Representation of a message catalog.""" + + def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER, + project=None, version=None, copyright_holder=None, + msgid_bugs_address=None, creation_date=None, + revision_date=None, last_translator=None, charset='utf-8'): + """Initialize the catalog object. + + :param locale: the locale identifier or `Locale` object, or `None` + if the catalog is not bound to a locale (which basically + means it's a template) + :param domain: the message domain + :param header_comment: the header comment as string, or `None` for the + default header + :param project: the project's name + :param version: the project's version + :param copyright_holder: the copyright holder of the catalog + :param msgid_bugs_address: the email address or URL to submit bug + reports to + :param creation_date: the date the catalog was created + :param revision_date: the date the catalog was revised + :param last_translator: the name and email of the last translator + :param charset: the encoding to use in the output + """ + self.domain = domain #: The message domain + if locale: + locale = Locale.parse(locale) + self.locale = locale #: The locale or `None` + self._header_comment = header_comment + self._messages = odict() + + self.project = project or 'PROJECT' #: The project name + self.version = version or 'VERSION' #: The project version + self.copyright_holder = copyright_holder or 'ORGANIZATION' + self.msgid_bugs_address = msgid_bugs_address or 'EMAIL@ADDRESS' + + self.last_translator = last_translator or 'FULL NAME <EMAIL@ADDRESS>' + """Name and email address of the last translator.""" + + self.charset = charset or 'utf-8' + + if creation_date is None: + creation_date = datetime.now(LOCALTZ) + elif isinstance(creation_date, datetime) and not creation_date.tzinfo: + creation_date = creation_date.replace(tzinfo=LOCALTZ) + self.creation_date = creation_date #: Creation date of the template + if revision_date is None: + revision_date = datetime.now(LOCALTZ) + elif isinstance(revision_date, datetime) and not revision_date.tzinfo: + revision_date = revision_date.replace(tzinfo=LOCALTZ) + self.revision_date = revision_date #: Last revision date of the catalog + + def _get_header_comment(self): + comment = self._header_comment + comment = comment.replace('PROJECT', self.project) \ + .replace('VERSION', self.version) \ + .replace('YEAR', self.revision_date.strftime('%Y')) \ + .replace('ORGANIZATION', self.copyright_holder) + if self.locale: + comment = comment.replace('Translations template', '%s translations' + % self.locale.english_name) + return comment + + def _set_header_comment(self, string): + self._header_comment = string + + header_comment = property(_get_header_comment, _set_header_comment, doc="""\ + The header comment for the catalog. + + >>> catalog = Catalog(project='Foobar', version='1.0', + ... copyright_holder='Foo Company') + >>> print catalog.header_comment + # Translations template for Foobar. + # Copyright (C) 2007 Foo Company + # This file is distributed under the same license as the Foobar project. + # FIRST AUTHOR <EMAIL@ADDRESS>, 2007. + # + + The header can also be set from a string. Any known upper-case variables + will be replaced when the header is retrieved again: + + >>> catalog = Catalog(project='Foobar', version='1.0', + ... copyright_holder='Foo Company') + >>> catalog.header_comment = '''\\ + ... # The POT for my really cool PROJECT project. + ... # Copyright (C) 1990-2003 ORGANIZATION + ... # This file is distributed under the same license as the PROJECT + ... # project. + ... #''' + >>> print catalog.header_comment + # The POT for my really cool Foobar project. + # Copyright (C) 1990-2003 Foo Company + # This file is distributed under the same license as the Foobar + # project. + # + + :type: `unicode` + """) + + def _get_mime_headers(self): + headers = [] + headers.append(('Project-Id-Version', + '%s %s' % (self.project, self.version))) + headers.append(('Report-Msgid-Bugs-To', self.msgid_bugs_address)) + headers.append(('POT-Creation-Date', + format_datetime(self.creation_date, 'yyyy-MM-dd HH:mmZ', + locale='en'))) + if self.locale is None: + headers.append(('PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE')) + headers.append(('Last-Translator', 'FULL NAME <EMAIL@ADDRESS>')) + headers.append(('Language-Team', 'LANGUAGE <LL@li.org>')) + else: + headers.append(('PO-Revision-Date', + format_datetime(self.revision_date, + 'yyyy-MM-dd HH:mmZ', locale='en'))) + headers.append(('Last-Translator', self.last_translator)) + headers.append(('Language-Team', '%s <LL@li.org>' % self.locale)) + headers.append(('Plural-Forms', self.plural_forms)) + headers.append(('MIME-Version', '1.0')) + headers.append(('Content-Type', + 'text/plain; charset=%s' % self.charset)) + headers.append(('Content-Transfer-Encoding', '8bit')) + headers.append(('Generated-By', 'Babel %s\n' % VERSION)) + return headers + + def _set_mime_headers(self, headers): + for name, value in headers: + name = name.lower() + if name == 'project-id-version': + parts = value.split(' ') + self.project = ' '.join(parts[:-1]) + self.version = parts[-1] + elif name == 'report-msgid-bugs-to': + self.msgid_bugs_address = value + elif name == 'last-translator': + self.last_translator = value + elif name == 'pot-creation-date': + # FIXME: this should use dates.parse_datetime as soon as that + # is ready + value, tzoffset, _ = re.split('[+-](\d{4})$', value, 1) + tt = time.strptime(value, '%Y-%m-%d %H:%M') + ts = time.mktime(tt) + tzoffset = FixedOffsetTimezone(int(tzoffset[:2]) * 60 + + int(tzoffset[2:])) + dt = datetime.fromtimestamp(ts) + self.creation_date = dt.replace(tzinfo=tzoffset) + + mime_headers = property(_get_mime_headers, _set_mime_headers, doc="""\ + The MIME headers of the catalog, used for the special ``msgid ""`` entry. + + The behavior of this property changes slightly depending on whether a locale + is set or not, the latter indicating that the catalog is actually a template + for actual translations. + + Here's an example of the output for such a catalog template: + + >>> created = datetime(1990, 4, 1, 15, 30, tzinfo=UTC) + >>> catalog = Catalog(project='Foobar', version='1.0', + ... creation_date=created) + >>> for name, value in catalog.mime_headers: + ... print '%s: %s' % (name, value) + Project-Id-Version: Foobar 1.0 + Report-Msgid-Bugs-To: EMAIL@ADDRESS + POT-Creation-Date: 1990-04-01 15:30+0000 + PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE + Last-Translator: FULL NAME <EMAIL@ADDRESS> + Language-Team: LANGUAGE <LL@li.org> + MIME-Version: 1.0 + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: 8bit + Generated-By: Babel ... + + And here's an example of the output when the locale is set: + + >>> revised = datetime(1990, 8, 3, 12, 0, tzinfo=UTC) + >>> catalog = Catalog(locale='de_DE', project='Foobar', version='1.0', + ... creation_date=created, revision_date=revised, + ... last_translator='John Doe <jd@example.com>') + >>> for name, value in catalog.mime_headers: + ... print '%s: %s' % (name, value) + Project-Id-Version: Foobar 1.0 + Report-Msgid-Bugs-To: EMAIL@ADDRESS + POT-Creation-Date: 1990-04-01 15:30+0000 + PO-Revision-Date: 1990-08-03 12:00+0000 + Last-Translator: John Doe <jd@example.com> + Language-Team: de_DE <LL@li.org> + Plural-Forms: nplurals=2; plural=(n != 1) + MIME-Version: 1.0 + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: 8bit + Generated-By: Babel ... + + :type: `list` + """) + + def num_plurals(self): + 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] + return num + num_plurals = property(num_plurals, doc="""\ + The number of plurals used by the locale. + + >>> Catalog(locale='en').num_plurals + 2 + >>> Catalog(locale='cs_CZ').num_plurals + 3 + + :type: `int` + """) + + def plural_forms(self): + num, expr = ('INTEGER', 'EXPRESSION') + if self.locale: + if str(self.locale) in PLURALS: + num, expr = PLURALS[str(self.locale)] + elif self.locale.language in PLURALS: + num, expr = PLURALS[self.locale.language] + return 'nplurals=%s; plural=%s' % (num, expr) + plural_forms = property(plural_forms, doc="""\ + Return the plural forms declaration for the locale. + + >>> Catalog(locale='en').plural_forms + 'nplurals=2; plural=(n != 1)' + >>> Catalog(locale='pt_BR').plural_forms + 'nplurals=2; plural=(n > 1)' + + :type: `str` + """) + + def __contains__(self, id): + """Return whether the catalog has a message with the specified ID.""" + return self._key_for(id) in self._messages + + def __len__(self): + """The number of messages in the catalog. + + This does not include the special ``msgid ""`` entry. + """ + return len(self._messages) + + def __iter__(self): + """Iterates through all the entries in the catalog, in the order they + were added, yielding a `Message` object for every entry. + + :rtype: ``iterator`` + """ + buf = [] + for name, value in self.mime_headers: + buf.append('%s: %s' % (name, value)) + yield Message('', '\n'.join(buf), flags=set(['fuzzy'])) + for key in self._messages: + yield self._messages[key] + + def __repr__(self): + locale = '' + if self.locale: + locale = ' %s' % self.locale + return '<%s %r%s>' % (type(self).__name__, self.domain, locale) + + def __delitem__(self, id): + """Delete the message with the specified ID.""" + key = self._key_for(id) + if key in self._messages: + del self._messages[key] + + def __getitem__(self, id): + """Return the message with the specified ID. + + :param id: the message ID + :return: the message with the specified ID, or `None` if no such message + is in the catalog + :rtype: `Message` + """ + return self._messages.get(self._key_for(id)) + + def __setitem__(self, id, message): + """Add or update the message with the specified ID. + + >>> catalog = Catalog() + >>> catalog[u'foo'] = Message(u'foo') + >>> catalog[u'foo'] + <Message u'foo'> + + If a message with that ID is already in the catalog, it is updated + to include the locations and flags of the new message. + + >>> catalog = Catalog() + >>> catalog[u'foo'] = Message(u'foo', locations=[('main.py', 1)]) + >>> catalog[u'foo'].locations + [('main.py', 1)] + >>> catalog[u'foo'] = Message(u'foo', locations=[('utils.py', 5)]) + >>> catalog[u'foo'].locations + [('main.py', 1), ('utils.py', 5)] + + :param id: the message ID + :param message: the `Message` object + """ + assert isinstance(message, Message), 'expected a Message object' + key = self._key_for(id) + current = self._messages.get(key) + if current: + if message.pluralizable and not current.pluralizable: + # The new message adds pluralization + current.id = message.id + current.string = message.string + current.locations.extend(message.locations) + current.auto_comments.extend(message.auto_comments) + current.user_comments.extend(message.user_comments) + current.flags |= message.flags + message = current + elif id == '': + # special treatment for the header message + headers = message_from_string(message.string.encode(self.charset)) + self.mime_headers = headers.items() + self.header_comment = '\n'.join(['# %s' % comment for comment + in message.user_comments]) + else: + if isinstance(id, (list, tuple)): + assert isinstance(message.string, (list, tuple)) + self._messages[key] = message + + def add(self, id, string=None, locations=(), flags=(), auto_comments=(), + user_comments=()): + """Add or update the message with the specified ID. + + >>> catalog = Catalog() + >>> catalog.add(u'foo') + >>> catalog[u'foo'] + <Message u'foo'> + + This method simply constructs a `Message` object with the given + arguments and invokes `__setitem__` with that object. + + :param id: the message ID, or a ``(singular, plural)`` tuple for + pluralizable messages + :param string: the translated message string, or a + ``(singular, plural)`` tuple for pluralizable messages + :param locations: a sequence of ``(filenname, lineno)`` tuples + :param flags: a set or sequence of flags + :param auto_comments: a sequence of automatic comments + :param user_comments: a sequence of user comments + """ + self[id] = Message(id, string, list(locations), flags, auto_comments, + user_comments) + + def _key_for(self, id): + """The key for a message is just the singular ID even for pluralizable + messages. + """ + key = id + if isinstance(key, (list, tuple)): + key = id[0] + return key
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/extract.py @@ -0,0 +1,310 @@ +# -*- 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 +try: + set +except NameError: + from sets import Set as set +import sys +from tokenize import generate_tokens, NAME, OP, STRING, COMMENT + +from babel.util import pathmatch, relpath + +__all__ = ['extract', 'extract_from_dir', 'extract_from_file'] +__docformat__ = 'restructuredtext en' + +GROUP_NAME = 'babel.extractors' + +DEFAULT_KEYWORDS = { + '_': None, + 'gettext': None, + 'ngettext': (1, 2), + 'ugettext': None, + 'ungettext': (1, 2), + 'dgettext': (2,), + 'dngettext': (2, 3), +} + +DEFAULT_MAPPING = [('**.py', 'python')] + +def extract_from_dir(dirname=os.getcwd(), method_map=DEFAULT_MAPPING, + options_map=None, keywords=DEFAULT_KEYWORDS, + comment_tags=(), callback=None): + """Extract messages from any source files found in the given directory. + + This function generates tuples of the form: + + ``(filename, lineno, message, comments)`` + + Which extraction method is used per file is determined by the `method_map` + parameter, which maps extended glob patterns to extraction method names. + For example, the following is the default mapping: + + >>> method_map = [ + ... ('**.py', 'python') + ... ] + + This basically says that files with the filename extension ".py" at any + level inside the directory should be processed by the "python" extraction + method. Files that don't match any of the mapping patterns are ignored. See + the documentation of the `pathmatch` function for details on the pattern + syntax. + + The following extended mapping would also use the "genshi" extraction + method on any file in "templates" subdirectory: + + >>> method_map = [ + ... ('**/templates/**.*', 'genshi'), + ... ('**.py', 'python') + ... ] + + The dictionary provided by the optional `options_map` parameter augments + these mappings. It uses extended glob patterns as keys, and the values are + dictionaries mapping options names to option values (both strings). + + The glob patterns of the `options_map` do not necessarily need to be the + same as those used in the method mapping. For example, while all files in + the ``templates`` folders in an application may be Genshi applications, the + options for those files may differ based on extension: + + >>> options_map = { + ... '**/templates/**.txt': { + ... 'template_class': 'genshi.template.text.TextTemplate', + ... 'encoding': 'latin-1' + ... }, + ... '**/templates/**.html': { + ... 'include_attrs': '' + ... } + ... } + + :param dirname: the path to the directory to extract messages from + :param method_map: a list of ``(pattern, method)`` tuples that maps of + extraction method names to extended glob patterns + :param options_map: a dictionary of additional options (optional) + :param keywords: a dictionary mapping keywords (i.e. names of functions + that should be recognized as translation functions) to + tuples that specify which of their arguments contain + localizable strings + :param comment_tags: a list of tags of translator comments to search for + and include in the results + :param callback: a function that is called for every file that message are + extracted from, just before the extraction itself is + performed; the function is passed the filename, the name + of the extraction method and and the options dictionary as + positional arguments, in that order + :return: an iterator over ``(filename, lineno, funcname, message)`` tuples + :rtype: ``iterator`` + :see: `pathmatch` + """ + if options_map is None: + options_map = {} + + absname = os.path.abspath(dirname) + for root, dirnames, filenames in os.walk(absname): + for subdir in dirnames: + if subdir.startswith('.') or subdir.startswith('_'): + dirnames.remove(subdir) + for filename in filenames: + filename = relpath( + os.path.join(root, filename).replace(os.sep, '/'), + dirname + ) + for pattern, method in method_map: + if pathmatch(pattern, filename): + filepath = os.path.join(absname, filename) + options = {} + for opattern, odict in options_map.items(): + if pathmatch(opattern, filename): + options = odict + if callback: + callback(filename, method, options) + for lineno, message, comments in \ + extract_from_file(method, filepath, + keywords=keywords, + comment_tags=comment_tags, + options=options): + yield filename, lineno, message, comments + break + +def extract_from_file(method, filename, keywords=DEFAULT_KEYWORDS, + comment_tags=(), 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 dictionary mapping keywords (i.e. names of functions + that should be recognized as translation functions) to + tuples that specify which of their arguments contain + localizable strings + :param comment_tags: a list of translator tags to search for and include + in the results + :param options: a dictionary of additional options (optional) + :return: the list of extracted messages + :rtype: `list` + """ + fileobj = open(filename, 'U') + try: + return list(extract(method, fileobj, keywords, comment_tags, options)) + finally: + fileobj.close() + +def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, comment_tags=(), + 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, message, comments)`` + + The implementation dispatches the actual extraction to plugins, based on the + value of the ``method`` parameter. + + >>> source = '''# foo module + ... def run(argv): + ... print _('Hello, world!') + ... ''' + + >>> from StringIO import StringIO + >>> for message in extract('python', StringIO(source)): + ... print message + (3, '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 dictionary mapping keywords (i.e. names of functions + that should be recognized as translation functions) to + tuples that specify which of their arguments contain + localizable strings + :param comment_tags: a list of translator tags to search for and include + in the results + :param options: a dictionary of additional options (optional) + :return: the list of extracted messages + :rtype: `list` + :raise ValueError: if the extraction method is not registered + """ + from pkg_resources import working_set + + for entry_point in working_set.iter_entry_points(GROUP_NAME, method): + func = entry_point.load(require=True) + results = func(fileobj, keywords.keys(), comment_tags, + options=options or {}) + for lineno, funcname, messages, comments in results: + if isinstance(messages, (list, tuple)): + msgs = [] + for index in keywords[funcname]: + msgs.append(messages[index - 1]) + messages = tuple(msgs) + if len(messages) == 1: + messages = messages[0] + yield lineno, messages, comments + return + + raise ValueError('Unknown extraction method %r' % method) + +def extract_nothing(fileobj, keywords, comment_tags, options): + """Pseudo extractor that does not actually extract anything, but simply + returns an empty list. + """ + return [] + +def extract_python(fileobj, keywords, comment_tags, options): + """Extract messages from Python source code. + + :param fileobj: the file-like object the messages should be extracted from + :param keywords: a list of keywords (i.e. function names) that should be + recognized as translation functions + :param comment_tags: a list of translator tags to search for and include + in the results + :param options: a dictionary of additional options (optional) + :return: an iterator over ``(lineno, funcname, message, comments)`` tuples + :rtype: ``iterator`` + """ + funcname = None + lineno = None + buf = [] + messages = [] + translator_comments = [] + in_args = False + in_translator_comments = False + + tokens = generate_tokens(fileobj.readline) + for tok, value, (lineno, _), _, _ in tokens: + if funcname and tok == OP and value == '(': + in_args = True + elif tok == COMMENT: + # Strip the comment token from the line + value = value[1:].strip() + if in_translator_comments is True and \ + translator_comments[-1][0] == lineno - 1: + # We're already inside a translator comment, continue appending + # XXX: Should we check if the programmer keeps adding the + # comment_tag for every comment line??? probably not! + translator_comments.append((lineno, value)) + continue + # If execution reaches this point, let's see if comment line + # starts with one of the comment tags + for comment_tag in comment_tags: + if value.startswith(comment_tag): + if in_translator_comments is not True: + in_translator_comments = True + comment = value[len(comment_tag):].strip() + translator_comments.append((lineno, comment)) + break + elif funcname and in_args: + if tok == OP and value == ')': + in_args = in_translator_comments = False + if buf: + messages.append(''.join(buf)) + del buf[:] + if filter(None, messages): + if len(messages) > 1: + messages = tuple(messages) + else: + messages = messages[0] + # Comments don't apply unless they immediately preceed the + # message + if translator_comments and \ + translator_comments[-1][0] < lineno - 1: + translator_comments = [] + + yield (lineno, funcname, messages, + [comment[1] for comment in translator_comments]) + funcname = lineno = None + messages = [] + translator_comments = [] + elif tok == STRING: + # Unwrap quotes in a safe manner + buf.append(eval(value, {'__builtins__':{}}, {})) + elif tok == OP and value == ',': + messages.append(''.join(buf)) + del buf[:] + elif funcname: + funcname = None + elif tok == NAME and value in keywords: + funcname = value
new file mode 100755 --- /dev/null +++ b/0.8.x/babel/messages/frontend.py @@ -0,0 +1,621 @@ +#!/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/. + +"""Frontends for the message extraction functionality.""" + +from ConfigParser import RawConfigParser +from datetime import datetime +from distutils import log +from distutils.cmd import Command +from distutils.errors import DistutilsOptionError, DistutilsSetupError +from optparse import OptionParser +import os +import re +from StringIO import StringIO +import sys + +from babel import __version__ as VERSION +from babel import Locale +from babel.core import UnknownLocaleError +from babel.messages.catalog import Catalog +from babel.messages.extract import extract_from_dir, DEFAULT_KEYWORDS, \ + DEFAULT_MAPPING +from babel.messages.pofile import read_po, write_po +from babel.messages.plurals import PLURALS +from babel.util import odict, LOCALTZ + +__all__ = ['CommandLineInterface', 'extract_messages', + 'check_message_extractors', '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.messages.frontend import extract_messages + + setup( + ... + cmdclass = {'extract_messages': extract_messages} + ) + + :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_ + :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ + """ + + description = 'extract localizable strings from the project code' + user_options = [ + ('charset=', None, + 'charset to use in the output file'), + ('keywords=', 'k', + 'space-separated list of keywords to look for in addition to the ' + 'defaults'), + ('no-default-keywords', None, + 'do not include the default keywords'), + ('mapping-file=', 'F', + 'path to the mapping configuration file'), + ('no-location', None, + 'do not include location comments with filename and line number'), + ('omit-header', None, + 'do not include msgid "" entry in header'), + ('output-file=', 'o', + 'name of the output file'), + ('width=', 'w', + 'set output line width (default 76)'), + ('no-wrap', None, + 'do not break long message lines, longer than the output line width, ' + 'into several lines'), + ('sort-output', None, + 'generate sorted output (default False)'), + ('sort-by-file', None, + 'sort output by file location (default False)'), + ('msgid-bugs-address=', None, + 'set report address for msgid'), + ('copyright-holder=', None, + 'set copyright holder in output'), + ('add-comments=', 'c', + 'place comment block with TAG (or those preceding keyword lines) in ' + 'output file. Seperate multiple TAGs with commas(,)'), + ('input-dirs=', None, + 'directories that should be scanned for messages'), + ] + boolean_options = [ + 'no-default-keywords', 'no-location', 'omit-header', 'no-wrap', + 'sort-output', 'sort-by-file' + ] + + def initialize_options(self): + self.charset = 'utf-8' + self.keywords = '' + self._keywords = DEFAULT_KEYWORDS.copy() + self.no_default_keywords = False + self.mapping_file = None + self.no_location = False + self.omit_header = False + self.output_file = None + self.input_dirs = None + self.width = 76 + self.no_wrap = False + self.sort_output = False + self.sort_by_file = False + self.msgid_bugs_address = None + self.copyright_holder = None + self.add_comments = None + self._add_comments = [] + + def finalize_options(self): + if self.no_default_keywords and not self.keywords: + raise DistutilsOptionError('you must specify new keywords if you ' + 'disable the default ones') + if self.no_default_keywords: + self._keywords = {} + if self.keywords: + self._keywords.update(parse_keywords(self.keywords.split())) + + if not self.output_file: + raise DistutilsOptionError('no output file specified') + if self.no_wrap and self.width: + raise DistutilsOptionError("'--no-wrap' and '--width' are mutually " + "exclusive") + if self.no_wrap: + self.width = None + else: + self.width = int(self.width) + + if self.sort_output and self.sort_by_file: + raise DistutilsOptionError("'--sort-output' and '--sort-by-file' " + "are mutually exclusive") + + if not self.input_dirs: + self.input_dirs = dict.fromkeys([k.split('.',1)[0] + for k in self.distribution.packages + ]).keys() + + if self.add_comments: + self._add_comments = self.add_comments.split(',') + + def run(self): + mappings = self._get_mappings() + outfile = open(self.output_file, 'w') + try: + catalog = Catalog(project=self.distribution.get_name(), + version=self.distribution.get_version(), + msgid_bugs_address=self.msgid_bugs_address, + copyright_holder=self.copyright_holder, + charset=self.charset) + + for dirname, (method_map, options_map) in mappings.items(): + def callback(filename, method, options): + if method == 'ignore': + return + filepath = os.path.normpath(os.path.join(dirname, filename)) + optstr = '' + if options: + optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for + k, v in options.items()]) + log.info('extracting messages from %s%s' + % (filepath, optstr)) + + extracted = extract_from_dir(dirname, method_map, options_map, + keywords=self._keywords, + comment_tags=self._add_comments, + callback=callback) + for filename, lineno, message, comments in extracted: + filepath = os.path.normpath(os.path.join(dirname, filename)) + catalog.add(message, None, [(filepath, lineno)], + auto_comments=comments) + + log.info('writing PO template file to %s' % self.output_file) + write_po(outfile, catalog, width=self.width, + no_location=self.no_location, + omit_header=self.omit_header, + sort_output=self.sort_output, + sort_by_file=self.sort_by_file) + finally: + outfile.close() + + def _get_mappings(self): + mappings = {} + + if self.mapping_file: + fileobj = open(self.mapping_file, 'U') + try: + method_map, options_map = parse_mapping(fileobj) + for dirname in self.input_dirs: + mappings[dirname] = method_map, options_map + finally: + fileobj.close() + + elif getattr(self.distribution, 'message_extractors', None): + message_extractors = self.distribution.message_extractors + for dirname, mapping in message_extractors.items(): + if isinstance(mapping, basestring): + method_map, options_map = parse_mapping(StringIO(mapping)) + else: + method_map, options_map = [], {} + for pattern, method, options in mapping: + method_map.append((pattern, method)) + options_map[pattern] = options or {} + mappings[dirname] = method_map, options_map + + else: + for dirname in self.input_dirs: + mappings[dirname] = DEFAULT_MAPPING, {} + + return mappings + + +def check_message_extractors(dist, name, value): + """Validate the ``message_extractors`` keyword argument to ``setup()``. + + :param dist: the distutils/setuptools ``Distribution`` object + :param name: the name of the keyword argument (should always be + "message_extractors") + :param value: the value of the keyword argument + :raise `DistutilsSetupError`: if the value is not valid + :see: `Adding setup() arguments + <http://peak.telecommunity.com/DevCenter/setuptools#adding-setup-arguments>`_ + """ + assert name == 'message_extractors' + if not isinstance(value, dict): + raise DistutilsSetupError('the value of the "message_extractors" ' + 'parameter must be a dictionary') + + +class new_catalog(Command): + """New catalog command for use in ``setup.py`` scripts. + + If correctly installed, this command is available to Setuptools-using + setup scripts automatically. For projects using plain old ``distutils``, + the command needs to be registered explicitly in ``setup.py``:: + + from babel.messages.frontend import new_catalog + + setup( + ... + cmdclass = {'new_catalog': new_catalog} + ) + + :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_ + :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ + """ + + description = 'create new catalogs based on a catalog template' + user_options = [ + ('domain=', 'D', + "domain of PO file (default 'messages')"), + ('input-file=', 'i', + 'name of the input file'), + ('output-dir=', 'd', + 'path to output directory'), + ('output-file=', 'o', + "name of the output file (default " + "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"), + ('locale=', 'l', + 'locale for the new localized catalog'), + ] + + def initialize_options(self): + self.output_dir = None + self.output_file = None + self.input_file = None + self.locale = None + self.domain = 'messages' + + def finalize_options(self): + if not self.input_file: + raise DistutilsOptionError('you must specify the input file') + + if not self.locale: + raise DistutilsOptionError('you must provide a locale for the ' + 'new catalog') + try: + self._locale = Locale.parse(self.locale) + except UnknownLocaleError, e: + raise DistutilsOptionError(e) + + if not self.output_file and not self.output_dir: + raise DistutilsOptionError('you must specify the output directory') + if not self.output_file: + self.output_file = os.path.join(self.output_dir, self.locale, + 'LC_MESSAGES', self.domain + '.po') + + if not os.path.exists(os.path.dirname(self.output_file)): + os.makedirs(os.path.dirname(self.output_file)) + + def run(self): + log.info('creating catalog %r based on %r', self.output_file, + self.input_file) + + infile = open(self.input_file, 'r') + try: + catalog = read_po(infile) + finally: + infile.close() + + catalog.locale = self._locale + + outfile = open(self.output_file, 'w') + try: + write_po(outfile, catalog) + finally: + outfile.close() + + +class CommandLineInterface(object): + """Command-line interface. + + This class provides a simple command-line interface to the message + extraction and PO file generation functionality. + """ + + usage = '%%prog %s [options] %s' + version = '%%prog %s' % VERSION + commands = ['extract', 'init'] + command_descriptions = { + 'extract': 'extract messages from source files and generate a POT file', + 'init': 'create new message catalogs from a template' + } + + def run(self, argv=sys.argv): + """Main entry point of the command-line interface. + + :param argv: list of arguments passed on the command-line + """ + self.parser = OptionParser(usage=self.usage % ('command', '[args]'), + version=self.version) + self.parser.disable_interspersed_args() + self.parser.print_help = self._help + options, args = self.parser.parse_args(argv[1:]) + if not args: + self.parser.error('incorrect number of arguments') + + cmdname = args[0] + if cmdname not in self.commands: + self.parser.error('unknown command "%s"' % cmdname) + + getattr(self, cmdname)(args[1:]) + + def _help(self): + print self.parser.format_help() + print "commands:" + longest = max([len(command) for command in self.commands]) + format = " %%-%ds %%s" % max(11, longest) + self.commands.sort() + for command in self.commands: + print format % (command, self.command_descriptions[command]) + + def extract(self, argv): + """Subcommand for extracting messages from source files and generating + a POT file. + + :param argv: the command arguments + """ + parser = OptionParser(usage=self.usage % ('extract', 'dir1 <dir2> ...'), + description=self.command_descriptions['extract']) + parser.add_option('--charset', dest='charset', + help='charset to use in the output') + parser.add_option('-k', '--keyword', dest='keywords', action='append', + help='keywords to look for in addition to the ' + 'defaults. You can specify multiple -k flags on ' + 'the command line.') + parser.add_option('--no-default-keywords', dest='no_default_keywords', + action='store_true', + help="do not include the default keywords") + parser.add_option('--mapping', '-F', dest='mapping_file', + help='path to the extraction mapping file') + parser.add_option('--no-location', dest='no_location', + action='store_true', + help='do not include location comments with filename ' + 'and line number') + parser.add_option('--omit-header', dest='omit_header', + action='store_true', + help='do not include msgid "" entry in header') + parser.add_option('-o', '--output', dest='output', + help='path to the output POT file') + parser.add_option('-w', '--width', dest='width', type='int', + help="set output line width (default %default)") + parser.add_option('--no-wrap', dest='no_wrap', action = 'store_true', + help='do not break long message lines, longer than ' + 'the output line width, into several lines') + parser.add_option('--sort-output', dest='sort_output', + action='store_true', + help='generate sorted output (default False)') + parser.add_option('--sort-by-file', dest='sort_by_file', + action='store_true', + help='sort output by file location (default False)') + parser.add_option('--msgid-bugs-address', dest='msgid_bugs_address', + metavar='EMAIL@ADDRESS', + help='set report address for msgid') + parser.add_option('--copyright-holder', dest='copyright_holder', + help='set copyright holder in output') + parser.add_option('--add-comments', '-c', dest='comment_tags', + metavar='TAG', action='append', + help='place comment block with TAG (or those ' + 'preceding keyword lines) in output file. One ' + 'TAG per argument call') + + parser.set_defaults(charset='utf-8', keywords=[], + no_default_keywords=False, no_location=False, + omit_header = False, width=76, no_wrap=False, + sort_output=False, sort_by_file=False, + comment_tags=[]) + options, args = parser.parse_args(argv) + if not args: + parser.error('incorrect number of arguments') + + if options.output not in (None, '-'): + outfile = open(options.output, 'w') + else: + outfile = sys.stdout + + keywords = DEFAULT_KEYWORDS.copy() + if options.no_default_keywords: + if not options.keywords: + parser.error('you must specify new keywords if you disable the ' + 'default ones') + keywords = {} + if options.keywords: + keywords.update(parse_keywords(options.keywords)) + + if options.mapping_file: + fileobj = open(options.mapping_file, 'U') + try: + method_map, options_map = parse_mapping(fileobj) + finally: + fileobj.close() + else: + method_map = DEFAULT_MAPPING + options_map = {} + + if options.width and options.no_wrap: + parser.error("'--no-wrap' and '--width' are mutually exclusive.") + elif not options.width and not options.no_wrap: + options.width = 76 + elif not options.width and options.no_wrap: + options.width = 0 + + if options.sort_output and options.sort_by_file: + parser.error("'--sort-output' and '--sort-by-file' are mutually " + "exclusive") + + try: + catalog = Catalog(msgid_bugs_address=options.msgid_bugs_address, + copyright_holder=options.copyright_holder, + charset=options.charset) + + for dirname in args: + if not os.path.isdir(dirname): + parser.error('%r is not a directory' % dirname) + extracted = extract_from_dir(dirname, method_map, options_map, + keywords, options.comment_tags) + for filename, lineno, message, comments in extracted: + filepath = os.path.normpath(os.path.join(dirname, filename)) + catalog.add(message, None, [(filepath, lineno)], + auto_comments=comments) + + write_po(outfile, catalog, width=options.width, + no_location=options.no_location, + omit_header=options.omit_header, + sort_output=options.sort_output, + sort_by_file=options.sort_by_file) + finally: + if options.output: + outfile.close() + + def init(self, argv): + """Subcommand for creating new message catalogs from a template. + + :param argv: the command arguments + """ + parser = OptionParser(usage=self.usage % ('init',''), + description=self.command_descriptions['init']) + parser.add_option('--domain', '-D', dest='domain', + help="domain of PO file (default '%default')") + parser.add_option('--input-file', '-i', dest='input_file', + metavar='FILE', help='name of the input file') + parser.add_option('--output-dir', '-d', dest='output_dir', + metavar='DIR', help='path to output directory') + parser.add_option('--output-file', '-o', dest='output_file', + metavar='FILE', + help="name of the output file (default " + "'<output_dir>/<locale>/LC_MESSAGES/" + "<domain>.po')") + parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', + help='locale for the new localized catalog') + + parser.set_defaults(domain='messages') + options, args = parser.parse_args(argv) + + if not options.locale: + parser.error('you must provide a locale for the new catalog') + try: + locale = Locale.parse(options.locale) + except UnknownLocaleError, e: + parser.error(e) + + if not options.input_file: + parser.error('you must specify the input file') + + if not options.output_file and not options.output_dir: + parser.error('you must specify the output file or directory') + + if not options.output_file: + options.output_file = os.path.join(options.output_dir, + options.locale, 'LC_MESSAGES', + options.domain + '.po') + if not os.path.exists(os.path.dirname(options.output_file)): + os.makedirs(os.path.dirname(options.output_file)) + + infile = open(options.input_file, 'r') + try: + catalog = read_po(infile) + finally: + infile.close() + + catalog.locale = locale + catalog.revision_date = datetime.now(LOCALTZ) + + print 'creating catalog %r based on %r' % (options.output_file, + options.input_file) + + outfile = open(options.output_file, 'w') + try: + write_po(outfile, catalog) + finally: + outfile.close() + +def main(): + CommandLineInterface().run(sys.argv) + +def parse_mapping(fileobj, filename=None): + """Parse an extraction method mapping from a file-like object. + + >>> buf = StringIO(''' + ... # Python source files + ... [python: **.py] + ... + ... # Genshi templates + ... [genshi: **/templates/**.html] + ... include_attrs = + ... [genshi: **/templates/**.txt] + ... template_class = genshi.template.text.TextTemplate + ... encoding = latin-1 + ... ''') + + >>> method_map, options_map = parse_mapping(buf) + + >>> method_map[0] + ('**.py', 'python') + >>> options_map['**.py'] + {} + >>> method_map[1] + ('**/templates/**.html', 'genshi') + >>> options_map['**/templates/**.html']['include_attrs'] + '' + >>> method_map[2] + ('**/templates/**.txt', 'genshi') + >>> options_map['**/templates/**.txt']['template_class'] + 'genshi.template.text.TextTemplate' + >>> options_map['**/templates/**.txt']['encoding'] + 'latin-1' + + :param fileobj: a readable file-like object containing the configuration + text to parse + :return: a `(method_map, options_map)` tuple + :rtype: `tuple` + :see: `extract_from_directory` + """ + method_map = [] + options_map = {} + + parser = RawConfigParser() + parser._sections = odict(parser._sections) # We need ordered sections + parser.readfp(fileobj, filename) + for section in parser.sections(): + method, pattern = [part.strip() for part in section.split(':', 1)] + method_map.append((pattern, method)) + options_map[pattern] = dict(parser.items(section)) + + return (method_map, options_map) + +def parse_keywords(strings=[]): + """Parse keywords specifications from the given list of strings. + + >>> kw = parse_keywords(['_', 'dgettext:2', 'dngettext:2,3']) + >>> for keyword, indices in sorted(kw.items()): + ... print (keyword, indices) + ('_', None) + ('dgettext', (2,)) + ('dngettext', (2, 3)) + """ + keywords = {} + for string in strings: + if ':' in string: + funcname, indices = string.split(':') + else: + funcname, indices = string, None + if funcname not in keywords: + if indices: + indices = tuple([(int(x)) for x in indices.split(',')]) + keywords[funcname] = indices + return keywords + + +if __name__ == '__main__': + main()
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/plurals.py @@ -0,0 +1,135 @@ +# -*- 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/. + +"""Plural form definitions.""" + +PLURALS = { + # Afrikaans - From Pootle's PO's + 'af': (2, '(n != 1)'), + # Arabic - From Pootle's PO's + 'ar': (6, '(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n>=3 && n<=10 ? 3 : n>=11 && n<=99 ? 4 : 5)'), + # Bulgarian - From Pootle's PO's + 'bg': (2, '(n != 1)'), + # Bengali - From Pootle's PO's + 'bn': (2, '(n != 1)'), + # Catalan - From Pootle's PO's + 'ca': (2, '(n != 1)'), + # Czech + 'cs': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Danish + 'da': (2, '(n != 1)'), + # German + 'de': (2, '(n != 1)'), + # Greek + 'el': (2, '(n != 1)'), + # English + 'en': (2, '(n != 1)'), + # Esperanto + 'eo': (2, '(n != 1)'), + # Spanish + 'es': (2, '(n != 1)'), + # Estonian + 'et': (2, '(n != 1)'), + # Basque - From Pootle's PO's + 'eu': (2, '(n != 1)'), + # Persian - From Pootle's PO's + 'fa': (1, '0'), + # Finnish + 'fi': (2, '(n != 1)'), + # French + 'fr': (2, '(n > 1)'), + # Furlan - From Pootle's PO's + 'fur': (2, '(n > 1)'), + # Irish + 'ga': (3, 'n==1 ? 0 : n==2 ? 1 : 2'), + # Galego - From Pootle's PO's + 'gl': (2, '(n != 1)'), + # Hausa - From Pootle's PO's + 'ha': (2, '(n != 1)'), + # Hebrew + 'he': (2, '(n != 1)'), + # Hindi - From Pootle's PO's + 'hi': (2, '(n != 1)'), + # Croatian + 'hr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Hungarian + 'hu': (1, '0'), + # Armenian - From Pootle's PO's + 'hy': (1, '0'), + # Icelandic - From Pootle's PO's + 'is': (2, '(n != 1)'), + # Italian + 'it': (2, '(n != 1)'), + # Japanese + 'ja': (1, '0'), + # Georgian - From Pootle's PO's + 'ka': (1, '0'), + # Kongo - From Pootle's PO's + 'kg': (2, '(n != 1)'), + # Khmer - From Pootle's PO's + 'km': (1, '0'), + # Korean + 'ko': (1, '0'), + # Kurdî - From Pootle's PO's + 'ku': (2, '(n != 1)'), + # Lithuanian + 'lt': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Latvian + 'lv': (3, '(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2)'), + # Maltese - From Pootle's PO's + 'mt': (4, '(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3)'), + # Norwegian Bokmal + 'nb': (2, '(n != 1)'), + # Dutch + 'nl': (2, '(n != 1)'), + # Norwegian Nynorsk + 'nn': (2, '(n != 1)'), + # Norwegian + 'no': (2, '(n != 1)'), + # Punjabi - From Pootle's PO's + 'pa': (2, '(n != 1)'), + # Polish + 'pl': (3, '(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Portuguese + 'pt': (2, '(n != 1)'), + # Brazilian + 'pt_BR': (2, '(n > 1)'), + # Romanian - From Pootle's PO's + 'ro': (3, '(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2)'), + # Russian + 'ru': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Slovak + 'sk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Slovenian + 'sl': (4, '(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3)'), + # Serbian - From Pootle's PO's + 'sr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10< =4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Sesotho - From Pootle's PO's + 'st': (2, '(n != 1)'), + # Swedish + 'sv': (2, '(n != 1)'), + # Turkish + 'tr': (1, '0'), + # Ukrainian + 'uk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Venda - From Pootle's PO's + 've': (2, '(n != 1)'), + # Vietnamese - From Pootle's PO's + 'vi': (1, '0'), + # Xhosa - From Pootle's PO's + 'xh': (2, '(n != 1)'), + # Chinese - From Pootle's PO's + 'zh_CN': (1, '0'), + 'zh_HK': (1, '0'), + 'zh_TW': (1, '0'), +}
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/pofile.py @@ -0,0 +1,374 @@ +# -*- 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>`_ +""" + +from datetime import date, datetime +import os +import re +try: + set +except NameError: + from sets import Set as set +from textwrap import wrap + +from babel import __version__ as VERSION +from babel.messages.catalog import Catalog +from babel.util import LOCALTZ + +__all__ = ['escape', 'normalize', 'read_po', 'write_po'] + +def read_po(fileobj): + """Read messages from a ``gettext`` PO (portable object) file from the given + file-like object and return a `Catalog`. + + >>> from StringIO import StringIO + >>> buf = StringIO(''' + ... #: main.py:1 + ... #, fuzzy, python-format + ... msgid "foo %(name)s" + ... msgstr "" + ... + ... # A user comment + ... #. An auto comment + ... #: main.py:3 + ... msgid "bar" + ... msgid_plural "baz" + ... msgstr[0] "" + ... msgstr[1] "" + ... ''') + >>> catalog = read_po(buf) + >>> catalog.revision_date = datetime(2007, 04, 01) + + >>> for message in catalog: + ... if message.id: + ... print (message.id, message.string) + ... print ' ', (message.locations, message.flags) + ... print ' ', (message.user_comments, message.auto_comments) + ('foo %(name)s', '') + ([('main.py', 1)], set(['fuzzy', 'python-format'])) + ([], []) + (('bar', 'baz'), ('', '')) + ([('main.py', 3)], set([])) + (['A user comment'], ['An auto comment']) + + :param fileobj: the file-like object to read the PO file from + :return: an iterator over ``(message, translation, location)`` tuples + :rtype: ``iterator`` + """ + catalog = Catalog() + + messages = [] + translations = [] + locations = [] + flags = [] + user_comments = [] + auto_comments = [] + in_msgid = in_msgstr = False + + def _add_message(): + translations.sort() + if len(messages) > 1: + 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]) + else: + string = denormalize(translations[0][1]) + catalog.add(msgid, string, list(locations), set(flags), + list(auto_comments), list(user_comments)) + del messages[:]; del translations[:]; del locations[:]; + del flags[:]; del auto_comments[:]; del user_comments[:] + + for line in fileobj.readlines(): + line = line.strip() + if line.startswith('#'): + in_msgid = in_msgstr = False + if messages: + _add_message() + if line[1:].startswith(':'): + for location in line[2:].lstrip().split(): + filename, lineno = location.split(':', 1) + locations.append((filename, int(lineno))) + elif line[1:].startswith(','): + for flag in line[2:].lstrip().split(','): + flags.append(flag.strip()) + elif line[1:].startswith('.'): + # These are called auto-comments + comment = line[2:].strip() + if comment: + # Just check that we're not adding empty comments + auto_comments.append(comment) + else: + # These are called user comments + user_comments.append(line[1:].strip()) + else: + if line.startswith('msgid_plural'): + in_msgid = True + msg = line[12:].lstrip() + messages.append(msg) + elif line.startswith('msgid'): + in_msgid = True + if messages: + _add_message() + messages.append(line[5:].lstrip()) + elif line.startswith('msgstr'): + in_msgid = False + in_msgstr = True + msg = line[6:].lstrip() + if msg.startswith('['): + idx, msg = msg[1:].split(']') + translations.append([int(idx), msg.lstrip()]) + else: + translations.append([0, msg]) + elif line.startswith('"'): + if in_msgid: + messages[-1] += u'\n' + line.rstrip() + elif in_msgstr: + translations[-1][1] += u'\n' + line.rstrip() + + if messages: + _add_message() + return catalog + +WORD_SEP = re.compile('(' + r'\s+|' # any whitespace + r'[^\s\w]*\w+[a-zA-Z]-(?=\w+[a-zA-Z])|' # hyphenated words + r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w)' # em-dash +')') + +def escape(string): + r"""Escape the given string so that it can be included in double-quoted + strings in ``PO`` files. + + >>> escape('''Say: + ... "hello, world!" + ... ''') + '"Say:\\n \\"hello, world!\\"\\n"' + + :param string: the string to escape + :return: the escaped string + :rtype: `str` or `unicode` + """ + return '"%s"' % string.replace('\\', '\\\\') \ + .replace('\t', '\\t') \ + .replace('\r', '\\r') \ + .replace('\n', '\\n') \ + .replace('\"', '\\"') + +def unescape(string): + r"""Reverse escape the given string. + + >>> print unescape('"Say:\\n \\"hello, world!\\"\\n"') + Say: + "hello, world!" + <BLANKLINE> + + :param string: the string to unescape + :return: the unescaped string + :rtype: `str` or `unicode` + """ + return string[1:-1].replace('\\\\', '\\') \ + .replace('\\t', '\t') \ + .replace('\\r', '\r') \ + .replace('\\n', '\n') \ + .replace('\\"', '\"') + +def normalize(string, width=76): + r"""Convert a string into a format that is appropriate for .po files. + + >>> print normalize('''Say: + ... "hello, world!" + ... ''', width=None) + "" + "Say:\n" + " \"hello, world!\"\n" + + >>> print normalize('''Say: + ... "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " + ... ''', width=32) + "" + "Say:\n" + " \"Lorem ipsum dolor sit " + "amet, consectetur adipisicing" + " elit, \"\n" + + :param string: the string to normalize + :param width: the maximum line width; use `None`, 0, or a negative number + to completely disable line wrapping + :return: the normalized string + :rtype: `unicode` + """ + if width and width > 0: + lines = [] + for idx, line in enumerate(string.splitlines(True)): + if len(escape(line)) > width: + chunks = WORD_SEP.split(line) + chunks.reverse() + while chunks: + buf = [] + size = 2 + while chunks: + l = len(escape(chunks[-1])) - 2 + if size + l < width: + buf.append(chunks.pop()) + size += l + else: + if not buf: + # handle long chunks by putting them on a + # separate line + buf.append(chunks.pop()) + break + lines.append(u''.join(buf)) + else: + lines.append(line) + else: + lines = string.splitlines(True) + + if len(lines) <= 1: + return escape(string) + + # Remove empty trailing line + if lines and not lines[-1]: + del lines[-1] + lines[-1] += '\n' + return u'""\n' + u'\n'.join([escape(l) for l in lines]) + +def denormalize(string): + r"""Reverse the normalization done by the `normalize` function. + + >>> print denormalize(r'''"" + ... "Say:\n" + ... " \"hello, world!\"\n"''') + Say: + "hello, world!" + <BLANKLINE> + + >>> print denormalize(r'''"" + ... "Say:\n" + ... " \"Lorem ipsum dolor sit " + ... "amet, consectetur adipisicing" + ... " elit, \"\n"''') + Say: + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " + <BLANKLINE> + + :param string: the string to denormalize + :return: the denormalized string + :rtype: `unicode` or `str` + """ + if string.startswith('""'): + lines = [] + for line in string.splitlines()[1:]: + lines.append(unescape(line)) + return ''.join(lines) + else: + return unescape(string) + +def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False, + sort_output=False, sort_by_file=False): + r"""Write a ``gettext`` PO (portable object) template file for a given + message catalog to the provided file-like object. + + >>> catalog = Catalog() + >>> catalog.add(u'foo %(name)s', locations=[('main.py', 1)], + ... flags=('fuzzy',)) + >>> catalog.add((u'bar', u'baz'), locations=[('main.py', 3)]) + >>> from StringIO import StringIO + >>> buf = StringIO() + >>> write_po(buf, catalog, omit_header=True) + >>> print buf.getvalue() + #: main.py:1 + #, fuzzy, python-format + msgid "foo %(name)s" + msgstr "" + <BLANKLINE> + #: main.py:3 + msgid "bar" + msgid_plural "baz" + msgstr[0] "" + msgstr[1] "" + <BLANKLINE> + <BLANKLINE> + + :param fileobj: the file-like object to write to + :param catalog: the `Catalog` instance + :param width: the maximum line width for the generated output; use `None`, + 0, or a negative number to completely disable line wrapping + :param no_location: do not emit a location comment for every message + :param omit_header: do not include the ``msgid ""`` entry at the top of the + output + """ + def _normalize(key): + return normalize(key, width=width).encode(catalog.charset, + 'backslashreplace') + + def _write(text): + if isinstance(text, unicode): + text = text.encode(catalog.charset) + fileobj.write(text) + + messages = list(catalog) + if sort_output: + messages.sort(lambda x,y: cmp(x.id, y.id)) + elif sort_by_file: + messages.sort(lambda x,y: cmp(x.locations, y.locations)) + + for message in messages: + if not message.id: # This is the header "message" + if omit_header: + continue + comment_header = catalog.header_comment + if width and width > 0: + lines = [] + for line in comment_header.splitlines(): + lines += wrap(line, width=width, subsequent_indent='# ', + break_long_words=False) + comment_header = u'\n'.join(lines) + u'\n' + _write(comment_header) + + if message.user_comments: + for comment in message.user_comments: + for line in wrap(comment, width, break_long_words=False): + _write('# %s\n' % line.strip()) + + if message.auto_comments: + for comment in message.auto_comments: + for line in wrap(comment, width, break_long_words=False): + _write('#. %s\n' % line.strip()) + + if not no_location: + locs = u' '.join([u'%s:%d' % (filename.replace(os.sep, '/'), lineno) + for filename, lineno in message.locations]) + if width and width > 0: + locs = wrap(locs, width, break_long_words=False) + for line in locs: + _write('#: %s\n' % line.strip()) + if message.flags: + _write('#%s\n' % ', '.join([''] + list(message.flags))) + + if isinstance(message.id, (list, tuple)): + _write('msgid %s\n' % _normalize(message.id[0])) + _write('msgid_plural %s\n' % _normalize(message.id[1])) + for i, string in enumerate(message.string): + _write('msgstr[%d] %s\n' % (i, _normalize(message.string[i]))) + else: + _write('msgid %s\n' % _normalize(message.id)) + _write('msgstr %s\n' % _normalize(message.string or '')) + _write('\n')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import unittest + +def suite(): + from babel.messages.tests import catalog, extract, frontend, pofile + suite = unittest.TestSuite() + suite.addTest(catalog.suite()) + suite.addTest(extract.suite()) + suite.addTest(frontend.suite()) + suite.addTest(pofile.suite()) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/catalog.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel.messages import catalog + + +class MessageTestCase(unittest.TestCase): + + def test_python_format(self): + assert catalog.PYTHON_FORMAT('foo %d bar') + assert catalog.PYTHON_FORMAT('foo %s bar') + assert catalog.PYTHON_FORMAT('foo %r bar') + + def test_translator_comments(self): + mess = catalog.Message('foo', user_comments=['Comment About `foo`']) + self.assertEqual(mess.user_comments, ['Comment About `foo`']) + mess = catalog.Message('foo', + auto_comments=['Comment 1 About `foo`', + 'Comment 2 About `foo`']) + self.assertEqual(mess.auto_comments, ['Comment 1 About `foo`', + 'Comment 2 About `foo`']) + + +class CatalogTestCase(unittest.TestCase): + + def test_two_messages_with_same_singular(self): + cat = catalog.Catalog() + cat.add('foo') + cat.add(('foo', 'foos')) + self.assertEqual(1, len(cat)) + + def test_update_message_updates_comments(self): + cat = catalog.Catalog() + cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 5)]) + self.assertEqual(cat[u'foo'].auto_comments, []) + self.assertEqual(cat[u'foo'].user_comments, []) + # Update cat[u'foo'] with a new location and a comment + cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 7)], + user_comments=['Foo Bar comment 1']) + self.assertEqual(cat[u'foo'].user_comments, ['Foo Bar comment 1']) + # now add yet another location with another comment + cat[u'foo'] = catalog.Message('foo', locations=[('main.py', 9)], + auto_comments=['Foo Bar comment 2']) + self.assertEqual(cat[u'foo'].auto_comments, ['Foo Bar comment 2']) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(catalog, optionflags=doctest.ELLIPSIS)) + suite.addTest(unittest.makeSuite(MessageTestCase)) + suite.addTest(unittest.makeSuite(CatalogTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/data/mapping.cfg @@ -0,0 +1,5 @@ +# Ignore CVS Dirs +[ignore: **/CVS/**.*] + +# Extraction from Python source files +[python: **.py]
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/data/project/CVS/a_test_file.txt @@ -0,0 +1,1 @@ +Just a test file.
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/data/project/CVS/this_wont_normally_be_here.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +# This file won't normally be in this directory. +# It IS only for tests + +from gettext import ngettext + +def foo(): + # Note: This will have the TRANSLATOR: tag but shouldn't + # be included on the extracted stuff + print ngettext('FooBar', 'FooBars', 1)
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/data/project/file1.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# file1.py for tests + +from gettext import gettext as _ +def foo(): + # TRANSLATOR: This will be a translator coment, + # that will include several lines + print _('bar')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/data/project/file2.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# file2.py for tests + +from gettext import ngettext + +def foo(): + # Note: This will have the TRANSLATOR: tag but shouldn't + # be included on the extracted stuff + print ngettext('foobar', 'foobars', 1)
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/data/project/i18n/messages.pot @@ -0,0 +1,32 @@ +# Translations template for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2007. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.1\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" +
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/data/setup.cfg @@ -0,0 +1,5 @@ +[extract_messages] +msgid_bugs_address = bugs.address@email.tld +copyright_holder = FooBar, TM +add_comments = TRANSLATOR:,TRANSLATORS: +output_file = project/i18n/project.pot
new file mode 100755 --- /dev/null +++ b/0.8.x/babel/messages/tests/data/setup.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# vim: sw=4 ts=4 fenc=utf-8 +# ============================================================================= +# $Id$ +# ============================================================================= +# $URL$ +# $LastChangedDate$ +# $Rev$ +# $LastChangedBy$ +# ============================================================================= +# Copyright (C) 2006 Ufsoft.org - Pedro Algarvio <ufs@ufsoft.org> +# +# Please view LICENSE for additional licensing information. +# ============================================================================= + +# THIS IS A BOGUS PROJECT + +from setuptools import setup, find_packages + +setup( + name = 'TestProject', + version = '0.1', + license = 'BSD', + author = 'Foo Bar', + author_email = 'foo@bar.tld', + packages = find_packages(), +)
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/extract.py @@ -0,0 +1,146 @@ +# -*- 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 StringIO import StringIO +import unittest + +from babel.messages import extract + + +class ExtractPythonTestCase(unittest.TestCase): + + def test_unicode_string_arg(self): + buf = StringIO("msg = _(u'Foo Bar')") + messages = list(extract.extract_python(buf, ('_',), [], {})) + self.assertEqual('Foo Bar', messages[0][2]) + + def test_comment_tag(self): + buf = StringIO(""" +# NOTE: A translation comment +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual('Foo Bar', messages[0][2]) + self.assertEqual(['A translation comment'], messages[0][3]) + + def test_comment_tag_multiline(self): + buf = StringIO(""" +# NOTE: A translation comment +# with a second line +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual('Foo Bar', messages[0][2]) + self.assertEqual(['A translation comment', 'with a second line'], + messages[0][3]) + + def test_translator_comments_with_previous_non_translator_comments(self): + buf = StringIO(""" +# This shouldn't be in the output +# because it didn't start with a comment tag +# NOTE: A translation comment +# with a second line +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual('Foo Bar', messages[0][2]) + self.assertEqual(['A translation comment', 'with a second line'], + messages[0][3]) + + def test_comment_tags_not_on_start_of_comment(self): + buf = StringIO(""" +# This shouldn't be in the output +# because it didn't start with a comment tag +# do NOTE: this will no be a translation comment +# NOTE: This one will be +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual('Foo Bar', messages[0][2]) + self.assertEqual(['This one will be'], messages[0][3]) + + def test_multiple_comment_tags(self): + buf = StringIO(""" +# NOTE1: A translation comment for tag1 +# with a second line +msg = _(u'Foo Bar1') + +# NOTE2: A translation comment for tag2 +msg = _(u'Foo Bar2') +""") + messages = list(extract.extract_python(buf, ('_',), + ['NOTE1:', 'NOTE2:'], {})) + self.assertEqual('Foo Bar1', messages[0][2]) + self.assertEqual(['A translation comment for tag1', + 'with a second line'], messages[0][3]) + self.assertEqual('Foo Bar2', messages[1][2]) + self.assertEqual(['A translation comment for tag2'], messages[1][3]) + + def test_two_succeeding_comments(self): + buf = StringIO(""" +# NOTE: one +# NOTE: two +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual('Foo Bar', messages[0][2]) + self.assertEqual(['one', 'NOTE: two'], messages[0][3]) + + def test_invalid_translator_comments(self): + buf = StringIO(""" +# NOTE: this shouldn't apply to any messages +hello = 'there' + +msg = _(u'Foo Bar') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual('Foo Bar', messages[0][2]) + self.assertEqual([], messages[0][3]) + + def test_invalid_translator_comments2(self): + buf = StringIO(""" +# NOTE: Hi! +hithere = _('Hi there!') + +# NOTE: you should not be seeing this in the .po +rows = [[v for v in range(0,10)] for row in range(0,10)] + +# this (NOTE:) should not show up either +hello = _('Hello') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual('Hi there!', messages[0][2]) + self.assertEqual(['Hi!'], messages[0][3]) + self.assertEqual('Hello', messages[1][2]) + self.assertEqual([], messages[1][3]) + + def test_invalid_translator_comments3(self): + buf = StringIO(""" +# NOTE: Hi, + +# there! +hithere = _('Hi there!') +""") + messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) + self.assertEqual('Hi there!', messages[0][2]) + self.assertEqual([], messages[0][3]) + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(extract)) + suite.addTest(unittest.makeSuite(ExtractPythonTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/frontend.py @@ -0,0 +1,536 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +from datetime import datetime +from distutils.dist import Distribution +from distutils.errors import DistutilsOptionError, DistutilsSetupError +from distutils.log import _global_log +import doctest +import os +import shutil +from StringIO import StringIO +import sys +import time +import unittest + +from babel import __version__ as VERSION +from babel.dates import format_datetime +from babel.messages import frontend +from babel.util import LOCALTZ + + +class ExtractMessagesTestCase(unittest.TestCase): + + def setUp(self): + self.olddir = os.getcwd() + self.datadir = os.path.join(os.path.dirname(__file__), 'data') + os.chdir(self.datadir) + _global_log.threshold = 5 # shut up distutils logging + + self.dist = Distribution(dict( + name='TestProject', + version='0.1', + packages=['project'] + )) + self.cmd = frontend.extract_messages(self.dist) + self.cmd.initialize_options() + + def tearDown(self): + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + if os.path.isfile(pot_file): + os.unlink(pot_file) + + os.chdir(self.olddir) + + def test_neither_default_nor_custom_keywords(self): + self.cmd.output_file = 'dummy' + self.cmd.no_default_keywords = True + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + def test_no_output_file_specified(self): + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + def test_both_sort_output_and_sort_by_file(self): + self.cmd.output_file = 'dummy' + self.cmd.sort_output = True + self.cmd.sort_by_file = True + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + def test_extraction_with_default_mapping(self): + self.cmd.copyright_holder = 'FooBar, Inc.' + self.cmd.msgid_bugs_address = 'bugs.address@email.tld' + self.cmd.output_file = 'project/i18n/temp.pot' + self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' + + self.cmd.finalize_options() + self.cmd.run() + + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + assert os.path.isfile(pot_file) + + self.assertEqual( +r"""# Translations template for TestProject. +# Copyright (C) %(year)s FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR <EMAIL@ADDRESS>, %(year)s. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: %(date)s\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +#: project/CVS/this_wont_normally_be_here.py:11 +msgid "FooBar" +msgid_plural "FooBars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'year': time.strftime('%Y'), + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(pot_file, 'U').read()) + + def test_extraction_with_mapping_file(self): + self.cmd.copyright_holder = 'FooBar, Inc.' + self.cmd.msgid_bugs_address = 'bugs.address@email.tld' + self.cmd.mapping_file = 'mapping.cfg' + self.cmd.output_file = 'project/i18n/temp.pot' + self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' + + self.cmd.finalize_options() + self.cmd.run() + + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + assert os.path.isfile(pot_file) + + self.assertEqual( +r"""# Translations template for TestProject. +# Copyright (C) %(year)s FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR <EMAIL@ADDRESS>, %(year)s. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: %(date)s\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'year': time.strftime('%Y'), + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(pot_file, 'U').read()) + + def test_extraction_with_mapping_dict(self): + self.dist.message_extractors = { + 'project': [ + ('**/CVS/**.*', 'ignore', None), + ('**.py', 'python', None), + ] + } + self.cmd.copyright_holder = 'FooBar, Inc.' + self.cmd.msgid_bugs_address = 'bugs.address@email.tld' + self.cmd.output_file = 'project/i18n/temp.pot' + self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' + + self.cmd.finalize_options() + self.cmd.run() + + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + assert os.path.isfile(pot_file) + + self.assertEqual( +r"""# Translations template for TestProject. +# Copyright (C) %(year)s FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR <EMAIL@ADDRESS>, %(year)s. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: %(date)s\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'year': time.strftime('%Y'), + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(pot_file, 'U').read()) + + +class NewCatalogTestCase(unittest.TestCase): + + def setUp(self): + self.olddir = os.getcwd() + self.datadir = os.path.join(os.path.dirname(__file__), 'data') + os.chdir(self.datadir) + _global_log.threshold = 5 # shut up distutils logging + + self.dist = Distribution(dict( + name='TestProject', + version='0.1', + packages=['project'] + )) + self.cmd = frontend.new_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_no_input_file(self): + self.cmd.locale = 'en_US' + self.cmd.output_file = 'dummy' + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + def test_no_locale(self): + self.cmd.input_file = 'dummy' + self.cmd.output_file = 'dummy' + self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) + + def test_with_output_dir(self): + self.cmd.input_file = 'project/i18n/messages.pot' + self.cmd.locale = 'en_US' + self.cmd.output_dir = 'project/i18n' + + self.cmd.finalize_options() + self.cmd.run() + + po_file = os.path.join(self.datadir, 'project', 'i18n', 'en_US', + 'LC_MESSAGES', 'messages.po') + assert os.path.isfile(po_file) + + self.assertEqual( +r"""# English (United States) translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2007. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: en_US <LL@li.org>\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(po_file, 'U').read()) + + +class CommandLineInterfaceTestCase(unittest.TestCase): + + def setUp(self): + self.datadir = os.path.join(os.path.dirname(__file__), 'data') + self.orig_argv = sys.argv + self.orig_stdout = sys.stdout + self.orig_stderr = sys.stderr + sys.argv = ['babel'] + sys.stdout = StringIO() + sys.stderr = StringIO() + self.cli = frontend.CommandLineInterface() + + def tearDown(self): + sys.argv = self.orig_argv + sys.stdout = self.orig_stdout + sys.stderr = self.orig_stderr + + def test_usage(self): + try: + self.cli.run(sys.argv) + self.fail('Expected SystemExit') + except SystemExit, e: + self.assertEqual(2, e.code) + self.assertEqual("""\ +usage: babel command [options] [args] + +babel: error: incorrect number of arguments +""", sys.stderr.getvalue().lower()) + + def test_help(self): + try: + self.cli.run(sys.argv + ['--help']) + self.fail('Expected SystemExit') + except SystemExit, e: + self.assertEqual(0, e.code) + self.assertEqual("""\ +usage: babel command [options] [args] + +options: + --version show program's version number and exit + -h, --help show this help message and exit + +commands: + extract extract messages from source files and generate a pot file + init create new message catalogs from a template +""", sys.stdout.getvalue().lower()) + + def test_extract_with_default_mapping(self): + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + try: + self.cli.run(sys.argv + ['extract', + '--copyright-holder', 'FooBar, Inc.', + '--msgid-bugs-address', 'bugs.address@email.tld', + '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:', + '-o', pot_file, os.path.join(self.datadir, 'project')]) + except SystemExit, e: + self.assertEqual(0, e.code) + assert os.path.isfile(pot_file) + self.assertEqual( +r"""# Translations template for TestProject. +# Copyright (C) %(year)s FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR <EMAIL@ADDRESS>, %(year)s. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: %(date)s\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +#: project/CVS/this_wont_normally_be_here.py:11 +msgid "FooBar" +msgid_plural "FooBars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'year': time.strftime('%Y'), + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(pot_file, 'U').read()) + + def test_extract_with_mapping_file(self): + pot_file = os.path.join(self.datadir, 'project', 'i18n', 'temp.pot') + try: + self.cli.run(sys.argv + ['extract', + '--copyright-holder', 'FooBar, Inc.', + '--msgid-bugs-address', 'bugs.address@email.tld', + '--mapping', os.path.join(self.datadir, 'mapping.cfg'), + '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:', + '-o', pot_file, os.path.join(self.datadir, 'project')]) + except SystemExit, e: + self.assertEqual(0, e.code) + assert os.path.isfile(pot_file) + self.assertEqual( +r"""# Translations template for TestProject. +# Copyright (C) %(year)s FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR <EMAIL@ADDRESS>, %(year)s. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: %(date)s\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'year': time.strftime('%Y'), + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(pot_file, 'U').read()) + + def test_init_with_output_dir(self): + po_file = os.path.join(self.datadir, 'project', 'i18n', 'en_US', + 'LC_MESSAGES', 'messages.po') + try: + self.cli.run(sys.argv + ['init', + '--locale', 'en_US', + '-d', os.path.join(self.datadir, 'project', 'i18n'), + '-i', os.path.join(self.datadir, 'project', 'i18n', + 'messages.pot')]) + except SystemExit, e: + self.assertEqual(0, e.code) + assert os.path.isfile(pot_file) + self.assertEqual( +r"""# English (United States) translations for TestProject. +# Copyright (C) 2007 FooBar, Inc. +# This file is distributed under the same license as the TestProject +# project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2007. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TestProject 0.1\n" +"Report-Msgid-Bugs-To: bugs.address@email.tld\n" +"POT-Creation-Date: 2007-04-01 15:30+0200\n" +"PO-Revision-Date: %(date)s\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: en_US <LL@li.org>\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel %(version)s\n" + +#. This will be a translator coment, +#. that will include several lines +#: project/file1.py:8 +msgid "bar" +msgstr "" + +#: project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +""" % {'version': VERSION, + 'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', + tzinfo=LOCALTZ, locale='en')}, + open(po_file, 'U').read()) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(frontend)) + suite.addTest(unittest.makeSuite(ExtractMessagesTestCase)) + suite.addTest(unittest.makeSuite(NewCatalogTestCase)) + suite.addTest(unittest.makeSuite(CommandLineInterfaceTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/messages/tests/pofile.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +from datetime import datetime +import doctest +from StringIO import StringIO +import unittest + +from babel.messages.catalog import Catalog +from babel.messages import pofile + + +class ReadPoTestCase(unittest.TestCase): + + def test_read_multiline(self): + buf = StringIO(r'''msgid "" +"Here's some text that\n" +"includesareallylongwordthatmightbutshouldnt" +" throw us into an infinite " +"loop\n" +msgstr ""''') + catalog = pofile.read_po(buf) + self.assertEqual(1, len(catalog)) + message = list(catalog)[1] + self.assertEqual("Here's some text that\nincludesareallylongwordthat" + "mightbutshouldnt throw us into an infinite loop\n", + message.id) + + +class WritePoTestCase(unittest.TestCase): + + def test_join_locations(self): + catalog = Catalog() + catalog.add(u'foo', locations=[('main.py', 1)]) + catalog.add(u'foo', locations=[('utils.py', 3)]) + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True) + self.assertEqual('''#: main.py:1 utils.py:3 +msgid "foo" +msgstr ""''', buf.getvalue().strip()) + + def test_wrap_long_lines(self): + text = """Here's some text where +white space and line breaks matter, and should + +not be removed + +""" + catalog = Catalog() + catalog.add(text, locations=[('main.py', 1)]) + buf = StringIO() + pofile.write_po(buf, catalog, no_location=True, omit_header=True, + width=42) + self.assertEqual(r'''msgid "" +"Here's some text where \n" +"white space and line breaks matter, and" +" should\n" +"\n" +"not be removed\n" +"\n" +msgstr ""''', buf.getvalue().strip()) + + def test_wrap_long_lines_with_long_word(self): + text = """Here's some text that +includesareallylongwordthatmightbutshouldnt throw us into an infinite loop +""" + catalog = Catalog() + catalog.add(text, locations=[('main.py', 1)]) + buf = StringIO() + pofile.write_po(buf, catalog, no_location=True, omit_header=True, + width=32) + self.assertEqual(r'''msgid "" +"Here's some text that\n" +"includesareallylongwordthatmightbutshouldnt" +" throw us into an infinite " +"loop\n" +msgstr ""''', buf.getvalue().strip()) + + def test_wrap_long_lines_in_header(self): + """ + Verify that long lines in the header comment are wrapped correctly. + """ + catalog = Catalog(project='AReallyReallyLongNameForAProject', + revision_date=datetime(2007, 4, 1)) + buf = StringIO() + pofile.write_po(buf, catalog) + self.assertEqual('''\ +# Translations template for AReallyReallyLongNameForAProject. +# Copyright (C) 2007 ORGANIZATION +# This file is distributed under the same license as the +# AReallyReallyLongNameForAProject project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2007. +# +#, fuzzy''', '\n'.join(buf.getvalue().splitlines()[:7])) + + def test_pot_with_translator_comments(self): + catalog = Catalog() + catalog.add(u'foo', locations=[('main.py', 1)], + auto_comments=['Comment About `foo`']) + catalog.add(u'bar', locations=[('utils.py', 3)], + user_comments=['Comment About `bar` with', + 'multiple lines.']) + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True) + self.assertEqual('''#. Comment About `foo` +#: main.py:1 +msgid "foo" +msgstr "" + +# Comment About `bar` with +# multiple lines. +#: utils.py:3 +msgid "bar" +msgstr ""''', buf.getvalue().strip()) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(pofile)) + suite.addTest(unittest.makeSuite(ReadPoTestCase)) + suite.addTest(unittest.makeSuite(WritePoTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/numbers.py @@ -0,0 +1,372 @@ +# -*- 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 default_locale, Locale + +__all__ = ['format_number', 'format_decimal', 'format_currency', + 'format_percent', 'format_scientific', 'parse_number', + 'parse_decimal', 'NumberFormatError'] +__docformat__ = 'restructuredtext en' + +LC_NUMERIC = default_locale('LC_NUMERIC') + +def get_currency_symbol(currency, locale=LC_NUMERIC): + """Return the symbol used by the locale for the specified currency. + + >>> get_currency_symbol('USD', 'en_US') + u'$' + + :param currency: the currency code + :param locale: the `Locale` object or locale identifier + :return: the currency symbol + :rtype: `unicode` + """ + return Locale.parse(locale).currency_symbols.get(currency, currency) + +def get_decimal_symbol(locale=LC_NUMERIC): + """Return the symbol used by the locale to separate decimal fractions. + + >>> get_decimal_symbol('en_US') + u'.' + + :param locale: the `Locale` object or locale identifier + :return: the decimal symbol + :rtype: `unicode` + """ + return Locale.parse(locale).number_symbols.get('decimal', u'.') + +def get_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): + """Return 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` + """ + # Do we really need this one? + return format_decimal(number, locale=locale) + +def format_decimal(number, format=None, locale=LC_NUMERIC): + """Return the given decimal number formatted for a specific locale. + + >>> format_decimal(1.2345, locale='en_US') + u'1.234' + >>> format_decimal(1.2346, locale='en_US') + u'1.235' + >>> format_decimal(-1.2346, locale='en_US') + u'-1.235' + >>> format_decimal(1.2345, locale='sv_SE') + u'1,234' + >>> format_decimal(12345, locale='de') + u'12.345' + + The appropriate thousands grouping and the decimal separator are used for + each locale: + + >>> format_decimal(12345.5, locale='en_US') + u'12,345.5' + + :param number: the number to format + :param format: + :param locale: the `Locale` object or locale identifier + :return: the formatted decimal number + :rtype: `unicode` + """ + locale = Locale.parse(locale) + if not format: + format = locale.decimal_formats.get(format) + pattern = parse_pattern(format) + return pattern.apply(number, locale) + +def format_currency(number, currency, format=None, locale=LC_NUMERIC): + u"""Return formatted currency value. + + >>> format_currency(1099.98, 'USD', locale='en_US') + u'$1,099.98' + >>> format_currency(1099.98, 'USD', locale='es_CO') + u'US$1.099,98' + >>> format_currency(1099.98, 'EUR', locale='de_DE') + u'1.099,98 \\u20ac' + + The pattern can also be specified explicitly: + + >>> format_currency(1099.98, 'EUR', u'\xa4\xa4 #,##0.00', locale='en_US') + u'EUR 1,099.98' + + :param number: the number to format + :param currency: the currency code + :param locale: the `Locale` object or locale identifier + :return: the formatted currency value + :rtype: `unicode` + """ + locale = Locale.parse(locale) + if not format: + format = locale.currency_formats.get(format) + pattern = parse_pattern(format) + return pattern.apply(number, locale, currency=currency) + +def format_percent(number, format=None, locale=LC_NUMERIC): + """Return formatted percent value for a specific locale. + + >>> format_percent(0.34, locale='en_US') + u'34%' + >>> format_percent(25.1234, locale='en_US') + u'2,512%' + >>> format_percent(25.1234, locale='sv_SE') + u'2\\xa0512 %' + + The format pattern can also be specified explicitly: + + >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US') + u'25,123\u2030' + + :param number: the percent number to format + :param format: + :param locale: the `Locale` object or locale identifier + :return: the formatted percent number + :rtype: `unicode` + """ + locale = Locale.parse(locale) + if not format: + format = locale.percent_formats.get(format) + pattern = parse_pattern(format) + return pattern.apply(number, locale) + +def format_scientific(number, locale=LC_NUMERIC): + # TODO: implement + raise NotImplementedError + + +class NumberFormatError(ValueError): + """Exception raised when a string cannot be parsed into a number.""" + + +def parse_number(string, locale=LC_NUMERIC): + """Parse localized number string into a long integer. + + >>> parse_number('1,099', locale='en_US') + 1099L + >>> parse_number('1.099', locale='de_DE') + 1099L + + When the given string cannot be parsed, an exception is raised: + + >>> parse_number('1.099,98', locale='de') + Traceback (most recent call last): + ... + NumberFormatError: '1.099,98' is not a valid number + + :param string: the string to parse + :param locale: the `Locale` object or locale identifier + :return: the parsed number + :rtype: `long` + :raise `NumberFormatError`: if the string can not be converted to a number + """ + try: + return long(string.replace(get_group_symbol(locale), '')) + except ValueError: + raise NumberFormatError('%r is not a valid number' % string) + +def parse_decimal(string, locale=LC_NUMERIC): + """Parse localized decimal string into a float. + + >>> parse_decimal('1,099.98', locale='en_US') + 1099.98 + >>> parse_decimal('1.099,98', locale='de') + 1099.98 + + When the given string cannot be parsed, an exception is raised: + + >>> parse_decimal('2,109,998', locale='de') + Traceback (most recent call last): + ... + NumberFormatError: '2,109,998' is not a valid decimal number + + :param string: the string to parse + :param locale: the `Locale` object or locale identifier + :return: the parsed decimal number + :rtype: `float` + :raise `NumberFormatError`: if the string can not be converted to a + decimal number + """ + locale = Locale.parse(locale) + try: + return float(string.replace(get_group_symbol(locale), '') + .replace(get_decimal_symbol(locale), '.')) + except ValueError: + raise NumberFormatError('%r is not a valid decimal number' % string) + + +PREFIX_END = r'[^0-9@#.,]' +NUMBER_TOKEN = r'[0-9@#.\-,E]' + +PREFIX_PATTERN = r"(?P<prefix>(?:'[^']*'|%s)*)" % PREFIX_END +NUMBER_PATTERN = r"(?P<number>%s+)" % NUMBER_TOKEN +SUFFIX_PATTERN = r"(?P<suffix>.*)" + +number_re = re.compile(r"%s%s%s" % (PREFIX_PATTERN, NUMBER_PATTERN, + SUFFIX_PATTERN)) + +# TODO: +# Filling +# Rounding increment in pattern +# Scientific notation +# Significant Digits +def parse_pattern(pattern): + """Parse number format patterns""" + if isinstance(pattern, NumberPattern): + return pattern + + # Do we have a negative subpattern? + if ';' in pattern: + pattern, neg_pattern = pattern.split(';', 1) + pos_prefix, number, pos_suffix = number_re.search(pattern).groups() + neg_prefix, _, neg_suffix = number_re.search(neg_pattern).groups() + else: + pos_prefix, number, pos_suffix = number_re.search(pattern).groups() + neg_prefix = '-' + pos_prefix + neg_suffix = pos_suffix + if '.' in number: + integer, fraction = number.rsplit('.', 1) + else: + integer = number + fraction = '' + min_frac = max_frac = 0 + + def parse_precision(p): + """Calculate the min and max allowed digits""" + min = max = 0 + for c in p: + if c == '0': + min += 1 + max += 1 + elif c == '#': + max += 1 + else: + break + return min, max + + def parse_grouping(p): + """Parse primary and secondary digit grouping + + >>> parse_grouping('##') + 0, 0 + >>> parse_grouping('#,###') + 3, 3 + >>> parse_grouping('#,####,###') + 3, 4 + """ + width = len(p) + g1 = p.rfind(',') + if g1 == -1: + return 1000, 1000 + g1 = width - g1 - 1 + g2 = p[:-g1 - 1].rfind(',') + if g2 == -1: + return g1, g1 + g2 = width - g1 - g2 - 2 + return g1, g2 + + int_precision = parse_precision(integer) + frac_precision = parse_precision(fraction) + grouping = parse_grouping(integer) + int_precision = (int_precision[0], 1000) # Unlimited + return NumberPattern(pattern, (pos_prefix, neg_prefix), + (pos_suffix, neg_suffix), grouping, + int_precision, frac_precision) + + +class NumberPattern(object): + + def __init__(self, pattern, prefix, suffix, grouping, + int_precision, frac_precision): + self.pattern = pattern + self.prefix = prefix + self.suffix = suffix + self.grouping = grouping + self.int_precision = int_precision + self.frac_precision = frac_precision + self.format = '%%#.%df' % self.frac_precision[1] + if '%' in ''.join(self.prefix + self.suffix): + self.scale = 100.0 + elif u'‰' in ''.join(self.prefix + self.suffix): + self.scale = 1000.0 + else: + self.scale = 1.0 + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.pattern) + + def apply(self, value, locale, currency=None): + value *= self.scale + negative = int(value < 0) + a, b = (self.format % abs(value)).split('.', 1) + retval = u'%s%s%s%s' % (self.prefix[negative], + self._format_int(a, locale), + self._format_frac(b, locale), + self.suffix[negative]) + if u'¤' in retval: + retval = retval.replace(u'¤¤', currency.upper()) + retval = retval.replace(u'¤', get_currency_symbol(currency, locale)) + return retval + + def _format_int(self, value, locale): + min, max = self.int_precision + width = len(value) + if width < min: + value += '0' * (min - width) + gsize = self.grouping[0] + ret = '' + symbol = get_group_symbol(locale) + while len(value) > gsize: + ret = symbol + value[-gsize:] + ret + value = value[:-gsize] + gsize = self.grouping[1] + return value + ret + + def _format_frac(self, value, locale): + min, max = self.frac_precision + if max == 0 or (min == 0 and int(value) == 0): + return '' + width = len(value) + while len(value) > min and value[-1] == '0': + value = value[:-1] + return get_decimal_symbol(locale) + value
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/support.py @@ -0,0 +1,320 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +"""Several classes and functions that help with integrating and using Babel +in applications. + +.. note: the code in this module is not used by Babel itself +""" + +from datetime import date, datetime, time +import gettext + +from babel.core import Locale +from babel.dates import format_date, format_datetime, format_time, LC_TIME +from babel.numbers import format_number, format_decimal, format_currency, \ + format_percent, format_scientific, LC_NUMERIC +from babel.util import UTC + +__all__ = ['Format', 'LazyProxy', 'Translations'] +__docformat__ = 'restructuredtext en' + + +class Format(object): + """Wrapper class providing the various date and number formatting functions + bound to a specific locale and time-zone. + + >>> fmt = Format('en_US', UTC) + >>> fmt.date(date(2007, 4, 1)) + u'Apr 1, 2007' + >>> fmt.decimal(1.2345) + u'1.234' + """ + + def __init__(self, locale, tzinfo=None): + """Initialize the formatter. + + :param locale: the locale identifier or `Locale` instance + :param tzinfo: the time-zone info (a `tzinfo` instance or `None`) + """ + self.locale = Locale.parse(locale) + self.tzinfo = tzinfo + + def date(self, date=None, format='medium'): + """Return a date formatted according to the given pattern. + + >>> fmt = Format('en_US') + >>> fmt.date(date(2007, 4, 1)) + u'Apr 1, 2007' + + :see: `babel.dates.format_date` + """ + return format_date(date, format, locale=self.locale) + + def datetime(self, datetime=None, format='medium'): + """Return a date and time formatted according to the given pattern. + + >>> from pytz import timezone + >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern')) + >>> fmt.datetime(datetime(2007, 4, 1, 15, 30)) + u'Apr 1, 2007 11:30:00 AM' + + :see: `babel.dates.format_datetime` + """ + return format_datetime(datetime, format, tzinfo=self.tzinfo, + locale=self.locale) + + def time(self, time=None, format='medium'): + """Return a time formatted according to the given pattern. + + >>> from pytz import timezone + >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern')) + >>> fmt.time(time(15, 30)) + u'11:30:00 AM' + + :see: `babel.dates.format_time` + """ + return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale) + + def number(self, number): + """Return an integer number formatted for the locale. + + >>> fmt = Format('en_US') + >>> fmt.number(1099) + u'1,099' + + :see: `babel.numbers.format_number` + """ + return format_number(number, locale=self.locale) + + def decimal(self, number, format=None): + """Return a decimal number formatted for the locale. + + >>> fmt = Format('en_US') + >>> fmt.decimal(1.2345) + u'1.234' + + :see: `babel.numbers.format_decimal` + """ + return format_decimal(number, format, locale=self.locale) + + def currency(self, number, currency): + """Return a number in the given currency formatted for the locale. + + :see: `babel.numbers.format_currency` + """ + return format_currency(number, currency, locale=self.locale) + + def percent(self, number, format=None): + """Return a number formatted as percentage for the locale. + + >>> fmt = Format('en_US') + >>> fmt.percent(0.34) + u'34%' + + :see: `babel.numbers.format_percent` + """ + return format_percent(number, format, locale=self.locale) + + def scientific(self, number): + """Return a number formatted using scientific notation for the locale. + + :see: `babel.numbers.format_scientific` + """ + return format_scientific(number, locale=self.locale) + + +class LazyProxy(object): + """Class for proxy objects that delegate to a specified function to evaluate + the actual object. + + >>> def greeting(name='world'): + ... return 'Hello, %s!' % name + >>> lazy_greeting = LazyProxy(greeting, name='Joe') + >>> print lazy_greeting + Hello, Joe! + >>> u' ' + lazy_greeting + u' Hello, Joe!' + >>> u'(%s)' % lazy_greeting + u'(Hello, Joe!)' + + This can be used, for example, to implement lazy translation functions that + delay the actual translation until the string is actually used. The + rationale for such behavior is that the locale of the user may not always + be available. In web applications, you only know the locale when processing + a request. + + The proxy implementation attempts to be as complete as possible, so that + the lazy objects should mostly work as expected, for example for sorting: + + >>> greetings = [ + ... LazyProxy(greeting, 'world'), + ... LazyProxy(greeting, 'Joe'), + ... LazyProxy(greeting, 'universe'), + ... ] + >>> greetings.sort() + >>> for greeting in greetings: + ... print greeting + Hello, Joe! + Hello, universe! + Hello, world! + """ + __slots__ = ['_func', '_args', '_kwargs', '_value'] + + def __init__(self, func, *args, **kwargs): + # Avoid triggering our own __setattr__ implementation + object.__setattr__(self, '_func', func) + object.__setattr__(self, '_args', args) + object.__setattr__(self, '_kwargs', kwargs) + object.__setattr__(self, '_value', None) + + def value(self): + if self._value is None: + value = self._func(*self._args, **self._kwargs) + object.__setattr__(self, '_value', value) + return self._value + value = property(value) + + def __contains__(self, key): + return key in self.value + + def __nonzero__(self): + return bool(self.value) + + def __dir__(self): + return dir(self.value) + + def __iter__(self): + return iter(self.value) + + def __len__(self): + return len(self.value) + + def __str__(self): + return str(self.value) + + def __unicode__(self): + return unicode(self.value) + + def __add__(self, other): + return self.value + other + + def __radd__(self, other): + return other + self.value + + def __mod__(self, other): + return self.value % other + + def __rmod__(self, other): + return other % self.value + + def __mul__(self, other): + return self.value * other + + def __rmul__(self, other): + return other * self.value + + def __call__(self, *args, **kwargs): + return self.value(*args, **kwargs) + + def __lt__(self, other): + return self.value < other + + def __le__(self, other): + return self.value <= other + + def __eq__(self, other): + return self.value == other + + def __ne__(self, other): + return self.value != other + + def __gt__(self, other): + return self.value > other + + def __ge__(self, other): + return self.value >= other + + def __delattr__(self, name): + delattr(self.value, name) + + def __getattr__(self, name): + return getattr(self.value, name) + + def __setattr__(self, name, value): + setattr(self.value, name, value) + + def __delitem__(self, key): + del self.value[key] + + def __getitem__(self, key): + return self.value[key] + + def __setitem__(self, key, value): + self.value[key] = value + + +class Translations(gettext.GNUTranslations): + """An extended translation catalog class.""" + + DEFAULT_DOMAIN = 'messages' + + def __init__(self, fileobj=None): + """Initialize the translations catalog. + + :param fileobj: the file-like object the translation should be read + from + """ + gettext.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` + """ + if not isinstance(locales, (list, tuple)): + locales = [locales] + locales = [str(locale) for locale in locales] + filename = gettext.find(domain or cls.DEFAULT_DOMAIN, dirname, locales) + if not filename: + return gettext.NullTranslations() + return cls(fileobj=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__)
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/tests/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import unittest + +def suite(): + from babel.tests import core, dates, localedata, numbers, support, util + from babel.messages import tests as messages + suite = unittest.TestSuite() + suite.addTest(core.suite()) + suite.addTest(dates.suite()) + suite.addTest(localedata.suite()) + suite.addTest(messages.suite()) + suite.addTest(numbers.suite()) + suite.addTest(support.suite()) + suite.addTest(util.suite()) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/tests/core.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel import core + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(core)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/tests/dates.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +from datetime import date, datetime, time +import doctest +import unittest + +from pytz import timezone + +from babel import dates + + +class DateTimeFormatTestCase(unittest.TestCase): + + def test_local_day_of_week(self): + d = datetime(2007, 4, 1) # a sunday + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('7', fmt['e']) # monday is first day of week + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('01', fmt['ee']) # sunday is first day of week + fmt = dates.DateTimeFormat(d, locale='dv_MV') + self.assertEqual('03', fmt['ee']) # friday is first day of week + + d = datetime(2007, 4, 2) # a monday + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('1', fmt['e']) # monday is first day of week + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('02', fmt['ee']) # sunday is first day of week + fmt = dates.DateTimeFormat(d, locale='dv_MV') + self.assertEqual('04', fmt['ee']) # friday is first day of week + + def test_local_day_of_week_standalone(self): + d = datetime(2007, 4, 1) # a sunday + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('7', fmt['c']) # monday is first day of week + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('1', fmt['c']) # sunday is first day of week + fmt = dates.DateTimeFormat(d, locale='dv_MV') + self.assertEqual('3', fmt['c']) # friday is first day of week + + d = datetime(2007, 4, 2) # a monday + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('1', fmt['c']) # monday is first day of week + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('2', fmt['c']) # sunday is first day of week + fmt = dates.DateTimeFormat(d, locale='dv_MV') + self.assertEqual('4', fmt['c']) # friday is first day of week + + def test_timezone_rfc822(self): + tz = timezone('Europe/Berlin') + t = time(15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(t, locale='de_DE') + self.assertEqual('+0100', fmt['Z']) + + def test_timezone_gmt(self): + tz = timezone('Europe/Berlin') + t = time(15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(t, locale='de_DE') + self.assertEqual('GMT +01:00', fmt['ZZZZ']) + + def test_timezone_walltime_short(self): + tz = timezone('Europe/Paris') + t = time(15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(t, locale='en_US') + self.assertEqual('CET', fmt['v']) + + def test_timezone_walltime_long(self): + tz = timezone('Europe/Paris') + t = time(15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(t, locale='en_US') + self.assertEqual('Central European Time', fmt['vvvv']) + + +class FormatDateTestCase(unittest.TestCase): + + def test_with_time_fields_in_pattern(self): + self.assertRaises(AttributeError, dates.format_date, date(2007, 04, 01), + "yyyy-MM-dd HH:mm", locale='en_US') + + def test_with_time_fields_in_pattern_and_datetime_param(self): + self.assertRaises(AttributeError, dates.format_date, + datetime(2007, 04, 01, 15, 30), + "yyyy-MM-dd HH:mm", locale='en_US') + + +class FormatTimeTestCase(unittest.TestCase): + + def test_with_date_fields_in_pattern(self): + self.assertRaises(AttributeError, dates.format_time, date(2007, 04, 01), + "yyyy-MM-dd HH:mm", locale='en_US') + + def test_with_date_fields_in_pattern_and_datetime_param(self): + self.assertRaises(AttributeError, dates.format_time, + datetime(2007, 04, 01, 15, 30), + "yyyy-MM-dd HH:mm", locale='en_US') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(dates)) + suite.addTest(unittest.makeSuite(DateTimeFormatTestCase)) + suite.addTest(unittest.makeSuite(FormatDateTestCase)) + suite.addTest(unittest.makeSuite(FormatTimeTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/tests/localedata.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel import localedata + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(localedata)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/tests/numbers.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel import numbers + + +class FormatDecimalTestCase(unittest.TestCase): + + def test_subpatterns(self): + self.assertEqual(numbers.format_decimal(-12345, '#,##0.##;-#', + locale='en_US'), '-12,345') + self.assertEqual(numbers.format_decimal(-12345, '#,##0.##;(#)', + locale='en_US'), '(12,345)') + + def test_default_rounding(self): + """ + Testing Round-Half-Even (Banker's rounding) + + A '5' is rounded to the closest 'even' number + """ + self.assertEqual(numbers.format_decimal(5.5, '0', locale='sv'), '6') + self.assertEqual(numbers.format_decimal(6.5, '0', locale='sv'), '6') + self.assertEqual(numbers.format_decimal(1.2325, locale='sv'), '1,232') + self.assertEqual(numbers.format_decimal(1.2335, locale='sv'), '1,234') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(numbers)) + suite.addTest(unittest.makeSuite(FormatDecimalTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/tests/support.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel import support + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(support)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/tests/util.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +import doctest +import unittest + +from babel import util + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(util)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/0.8.x/babel/util.py @@ -0,0 +1,222 @@ +# -*- 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.""" + +from datetime import timedelta, tzinfo +import os +import re +import time + +__all__ = ['pathmatch', 'relpath', 'UTC', 'LOCALTZ'] +__docformat__ = 'restructuredtext en' + +def pathmatch(pattern, filename): + """Extended pathname pattern matching. + + This function is similar to what is provided by the ``fnmatch`` module in + the Python standard library, but: + + * can match complete (relative or absolute) path names, and not just file + names, and + * also supports a convenience pattern ("**") to match files at any + directory level. + + Examples: + + >>> pathmatch('**.py', 'bar.py') + True + >>> pathmatch('**.py', 'foo/bar/baz.py') + True + >>> pathmatch('**.py', 'templates/index.html') + False + + >>> pathmatch('**/templates/*.html', 'templates/index.html') + True + >>> pathmatch('**/templates/*.html', 'templates/foo/bar.html') + False + + :param pattern: the glob pattern + :param filename: the path name of the file to match against + :return: `True` if the path name matches the pattern, `False` otherwise + :rtype: `bool` + """ + symbols = { + '?': '[^/]', + '?/': '[^/]/', + '*': '[^/]+', + '*/': '[^/]+/', + '**/': '(?:.+/)*?', + '**': '(?:.+/)*?[^/]+', + } + buf = [] + for idx, part in enumerate(re.split('([?*]+/?)', pattern)): + if idx % 2: + buf.append(symbols[part]) + elif part: + buf.append(re.escape(part)) + match = re.match(''.join(buf) + '$', filename.replace(os.sep, '/')) + return match is not None + + +class odict(dict): + """Ordered dict implementation. + + :see: `http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747` + """ + def __init__(self, data=None): + dict.__init__(self, data or {}) + self._keys = [] + + def __delitem__(self, key): + dict.__delitem__(self, key) + self._keys.remove(key) + + def __setitem__(self, key, item): + dict.__setitem__(self, key, item) + if key not in self._keys: + self._keys.append(key) + + def __iter__(self): + return iter(self._keys) + + def clear(self): + dict.clear(self) + self._keys = [] + + def copy(self): + d = odict() + d.update(self) + return d + + def items(self): + return zip(self._keys, self.values()) + + def keys(self): + return self._keys[:] + + def setdefault(self, key, failobj = None): + dict.setdefault(self, key, failobj) + if key not in self._keys: + self._keys.append(key) + + def update(self, dict): + for (key, val) in dict.items(): + self[key] = val + + def values(self): + return map(self.get, self._keys) + + +try: + relpath = os.path.relpath +except AttributeError: + def relpath(path, start='.'): + """Compute the relative path to one path from another. + + >>> relpath('foo/bar.txt', '').replace(os.sep, '/') + 'foo/bar.txt' + >>> relpath('foo/bar.txt', 'foo').replace(os.sep, '/') + 'bar.txt' + >>> relpath('foo/bar.txt', 'baz').replace(os.sep, '/') + '../foo/bar.txt' + + :return: the relative path + :rtype: `basestring` + """ + start_list = os.path.abspath(start).split(os.sep) + path_list = os.path.abspath(path).split(os.sep) + + # Work out how much of the filepath is shared by start and path. + i = len(os.path.commonprefix([start_list, path_list])) + + rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:] + return os.path.join(*rel_list) + +ZERO = timedelta(0) + + +class FixedOffsetTimezone(tzinfo): + """Fixed offset in minutes east from UTC.""" + + def __init__(self, offset, name=None): + self._offset = timedelta(minutes=offset) + if name is None: + name = 'Etc/GMT+%d' % offset + self.zone = name + + def __str__(self): + return self.zone + + def __repr__(self): + return '<FixedOffset "%s" %s>' % (self.zone, self._offset) + + def utcoffset(self, dt): + return self._offset + + def tzname(self, dt): + return self.zone + + def dst(self, dt): + return ZERO + + +try: + from pytz import UTC +except ImportError: + UTC = FixedOffsetTimezone(0, 'UTC') + """`tzinfo` object for UTC (Universal Time). + + :type: `tzinfo` + """ + +STDOFFSET = timedelta(seconds = -time.timezone) +if time.daylight: + DSTOFFSET = timedelta(seconds = -time.altzone) +else: + DSTOFFSET = STDOFFSET + +DSTDIFF = DSTOFFSET - STDOFFSET + + +class LocalTimezone(tzinfo): + + def utcoffset(self, dt): + if self._isdst(dt): + return DSTOFFSET + else: + return STDOFFSET + + def dst(self, dt): + if self._isdst(dt): + return DSTDIFF + else: + return ZERO + + def tzname(self, dt): + return time.tzname[self._isdst(dt)] + + def _isdst(self, dt): + tt = (dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + dt.weekday(), 0, -1) + stamp = time.mktime(tt) + tt = time.localtime(stamp) + return tt.tm_isdst > 0 + + +LOCALTZ = LocalTimezone() +"""`tzinfo` object for local time-zone. + +:type: `tzinfo` +"""
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/cmdline.txt @@ -0,0 +1,107 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +====================== +Command-Line Interface +====================== + +Babel includes a command-line interface for working with message catalogs, +similar to the various GNU ``gettext`` tools commonly available on Linux/Unix +systems. + + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +When properly installed, Babel provides a script called ``babel``:: + + $ babel --help + usage: babel subcommand [options] [args] + + options: + --version show program's version number and exit + -h, --help show this help message and exit + + subcommands: + extract extract messages from source files and generate a POT file + init create new message catalogs from a template + +The ``babel`` script provides a number of sub-commands that do the actual work. +Those sub-commands are described below. + + +extract +======= + +The ``extract`` sub-command can be used to extract localizable messages from +a collection of source files:: + + $ babel extract --help + usage: babel extract [options] dir1 <dir2> ... + + extract messages from source files and generate a POT file + + options: + -h, --help show this help message and exit + --charset=CHARSET charset to use in the output + -k KEYWORDS, --keyword=KEYWORDS + keywords to look for in addition to the defaults. You + can specify multiple -k flags on the command line. + --no-default-keywords + do not include the default keywords + -F MAPPING_FILE, --mapping=MAPPING_FILE + path to the extraction mapping file + --no-location do not include location comments with filename and + line number + --omit-header do not include msgid "" entry in header + -o OUTPUT, --output=OUTPUT + path to the output POT file + -w WIDTH, --width=WIDTH + set output line width (default 76) + --no-wrap do not break long message lines, longer than the + output line width, into several lines + --sort-output generate sorted output (default False) + --sort-by-file sort output by file location (default False) + --msgid-bugs-address=EMAIL@ADDRESS + set report address for msgid + --copyright-holder=COPYRIGHT_HOLDER + set copyright holder in output + -c TAG, --add-comments=TAG + place comment block with TAG (or those preceding + keyword lines) in output file. One TAG per argument + call + + +init +==== + +The `init` sub-command creates a new translations catalog based on a PO +template file:: + + $ babel init --help + usage: babel init [options] + + create new message catalogs from a template + + options: + -h, --help show this help message and exit + -D DOMAIN, --domain=DOMAIN + domain of PO file (defaults to lower-cased project + name) + -i INPUT_FILE, --input-file=INPUT_FILE + name of the input file + -d OUTPUT_DIR, --output-dir=OUTPUT_DIR + path to output directory + -o OUTPUT_FILE, --output-file=OUTPUT_FILE + name of the output file (default + '<output_dir>/<locale>/<domain>.po') + -l LOCALE, --locale=LOCALE + locale for the new localized catalog + --first-author=FIRST_AUTHOR_NAME + name of first author + --first-author-email=FIRST_AUTHOR_EMAIL + email of first author + --project-name=NAME the project name + --project-version=VERSION + the project version
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/conf/docutils.ini @@ -0,0 +1,9 @@ +[general] +input_encoding = utf-8 +strip_comments = yes +toc_backlinks = none + +[html4css1 writer] +embed_stylesheet = no +stylesheet = style/edgewall.css +xml_declaration = no
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/conf/epydoc.ini @@ -0,0 +1,24 @@ +[epydoc] + +name: Documentation Index +url: ../index.html +modules: babel +verbosity: 1 + +# Extraction +docformat: restructuredtext +parse: yes +introspect: yes +exclude: .*\.tests.* +inheritance: listed +private: no +imports: no +include-log: no + +# HTML output +output: html +target: doc/api/ +css: doc/style/epydoc.css +top: babel +frames: no +sourcecode: no
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/dates.txt @@ -0,0 +1,249 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +=============== +Date Formatting +=============== + + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +When working with date and time information in Python, you commonly use the +classes ``date``, ``datetime`` and/or ``time`` from the `datetime`_ package. +Babel provides functions for locale-specific formatting of those objects in its +``dates`` module: + +.. _`datetime`: http://docs.python.org/lib/module-datetime.html + +.. code-block:: pycon + + >>> from datetime import date, datetime, time + >>> from babel.dates import format_date, format_datetime, format_time + + >>> d = date(2007, 4, 1) + >>> format_date(d, locale='en') + u'Apr 1, 2007' + >>> format_date(d, locale='de_DE') + u'01.04.2007' + +As this example demonstrates, Babel will automatically choose a date format +that is appropriate for the requested locale. + +The ``format_*()`` functions also accept an optional ``format`` argument, which +allows you to choose between one of four format variations: + + * ``short``, + * ``medium`` (the default), + * ``long``, and + * ``full``. + +For example: + +.. code-block:: pycon + + >>> format_date(d, format='short', locale='en') + u'4/1/07' + >>> format_date(d, format='long', locale='en') + u'April 1, 2007' + >>> format_date(d, format='full', locale='en') + u'Sunday, April 1, 2007' + + +Pattern Syntax +============== + +While Babel makes it simple to use the appropriate date/time format for a given +locale, you can also force it to use custom patterns. Note that Babel uses +different patterns for specifying number and date formats compared to the +Python equivalents (such as ``time.strftime()``), which have mostly been +inherited from C and POSIX. The patterns used in Babel are based on the +`Locale Data Markup Language specification`_ (LDML), which defines them as +follows: + + A date/time pattern is a string of characters, where specific strings of + characters are replaced with date and time data from a calendar when formatting + or used to generate data for a calendar when parsing. […] + + Characters may be used multiple times. For example, if ``y`` is used for the + year, ``yy`` might produce "99", whereas ``yyyy`` produces "1999". For most + numerical fields, the number of characters specifies the field width. For + example, if ``h`` is the hour, ``h`` might produce "5", but ``hh`` produces + "05". For some characters, the count specifies whether an abbreviated or full + form should be used […] + + Two single quotes represent a literal single quote, either inside or outside + single quotes. Text within single quotes is not interpreted in any way (except + for two adjacent single quotes). + +For example: + +.. code-block:: pycon + + >>> d = date(2007, 4, 1) + >>> format_date(d, "EEE, MMM d, ''yy", locale='en') + u"Sun, Apr 1, '07" + >>> format_date(d, "EEEE, d.M.yyyy", locale='de') + u'Sonntag, 1.4.2007' + + >>> t = time(15, 30) + >>> format_time(t, "hh 'o''clock' a", locale='en') + u"03 o'clock PM" + >>> format_time(t, 'H:mm a', locale='de') + u'15:30 nachm.' + + >>> dt = datetime(2007, 4, 1, 15, 30) + >>> format_datetime(dt, "yyyyy.MMMM.dd GGG hh:mm a", locale='en') + u'02007.April.01 AD 03:30 PM' + +The syntax for custom datetime format patterns is described in detail in the +the `Locale Data Markup Language specification`_. The following table is just a +relatively brief overview. + + .. _`Locale Data Markup Language specification`: http://unicode.org/reports/tr35/#Date_Format_Patterns + +Date Fields +----------- + + +----------+--------+--------------------------------------------------------+ + | Field | Symbol | Description | + +==========+========+========================================================+ + | Era | ``G`` | Replaced with the era string for the current date. One | + | | | to three letters for the abbreviated form, four | + | | | lettersfor the long form, five for the narrow form | + +----------+--------+--------------------------------------------------------+ + | Year | ``y`` | Replaced by the year. Normally the length specifies | + | | | the padding, but for two letters it also specifies the | + | | | maximum length. | + | +--------+--------------------------------------------------------+ + | | ``Y`` | Same as ``y`` but uses the ISO year-week calendar. | + | +--------+--------------------------------------------------------+ + | | ``u`` | ?? | + +----------+--------+--------------------------------------------------------+ + | Quarter | ``Q`` | Use one or two for the numerical quarter, three for | + | | | the abbreviation, or four for the full name. | + | +--------+--------------------------------------------------------+ + | | ``q`` | Use one or two for the numerical quarter, three for | + | | | the abbreviation, or four for the full name. | + +----------+--------+--------------------------------------------------------+ + | Month | ``M`` | Use one or two for the numerical month, three for the | + | | | abbreviation, or four for the full name, or five for | + | | | the narrow name. | + | +--------+--------------------------------------------------------+ + | | ``L`` | Use one or two for the numerical month, three for the | + | | | abbreviation, or four for the full name, or 5 for the | + | | | narrow name. | + +----------+--------+--------------------------------------------------------+ + | Week | ``w`` | Week of year. | + | +--------+--------------------------------------------------------+ + | | ``W`` | Week of month. | + +----------+--------+--------------------------------------------------------+ + | Day | ``d`` | Day of month. | + | +--------+--------------------------------------------------------+ + | | ``D`` | Day of year. | + | +--------+--------------------------------------------------------+ + | | ``F`` | Day of week in month. | + | +--------+--------------------------------------------------------+ + | | ``g`` | ?? | + +----------+--------+--------------------------------------------------------+ + | Week day | ``E`` | Day of week. Use one through three letters for the | + | | | short day, or four for the full name, or five for the | + | | | narrow name. | + | +--------+--------------------------------------------------------+ + | | ``e`` | Local day of week. Same as E except adds a numeric | + | | | value that will depend on the local starting day of | + | | | the week, using one or two letters. | + | +--------+--------------------------------------------------------+ + | | ``c`` | ?? | + +----------+--------+--------------------------------------------------------+ + +Time Fields +----------- + + +----------+--------+--------------------------------------------------------+ + | Field | Symbol | Description | + +==========+========+========================================================+ + | Period | ``a`` | AM or PM | + +----------+--------+--------------------------------------------------------+ + | Hour | ``h`` | Hour [1-12]. | + | +--------+--------------------------------------------------------+ + | | ``H`` | Hour [0-23]. | + | +--------+--------------------------------------------------------+ + | | ``K`` | Hour [0-11]. | + | +--------+--------------------------------------------------------+ + | | ``k`` | Hour [1-24]. | + +----------+--------+--------------------------------------------------------+ + | Minute | ``m`` | Use one or two for zero places padding. | + +----------+--------+--------------------------------------------------------+ + | Second | ``s`` | Use one or two for zero places padding. | + | +--------+--------------------------------------------------------+ + | | ``S`` | Fractional second, rounds to the count of letters. | + | +--------+--------------------------------------------------------+ + | | ``A`` | Milliseconds in day. | + +----------+--------+--------------------------------------------------------+ + | Timezone | ``z`` | Use one to three letters for the short timezone or | + | | | four for the full name. | + | +--------+--------------------------------------------------------+ + | | ``Z`` | Use one to three letters for RFC 822, four letters for | + | | | GMT format. | + | +--------+--------------------------------------------------------+ + | | ``v`` | Use one letter for short wall (generic) time, four for | + | | | long wall time. | + +----------+--------+--------------------------------------------------------+ + + +Time-Zone Support +================= + +Many of the verbose time formats include the time-zone, but time-zone +information is not by default available for the Python ``datetime`` and +``time`` objects. The standard library includes only the abstract ``tzinfo`` +class, which you need appropriate implementations for to actually use in your +application. Babel includes a ``tzinfo`` implementation for UTC (Universal +Time). + +For real time-zone support, it is strongly recommended that you use the +third-party package `pytz`_, which includes the definitions of practically all +of the time-zones used on the world, as well as important functions for +reliably converting from UTC to local time, and vice versa: + +.. code-block:: pycon + + >>> from datetime import time + >>> from pytz import timezone, utc + >>> dt = datetime(2007, 04, 01, 15, 30, tzinfo=utc) + >>> eastern = timezone('US/Eastern') + >>> format_datetime(dt, 'H:mm Z', tzinfo=eastern, locale='en_US') + u'11:30 -0400' + +The recommended approach to deal with different time-zones in a Python +application is to always use UTC internally, and only convert from/to the users +time-zone when accepting user input and displaying date/time data, respectively. +You can use Babel together with ``pytz`` to apply a time-zone to any +``datetime`` or ``time`` object for display, leaving the original information +unchanged: + +.. code-block:: pycon + + >>> british = timezone('Europe/London') + >>> format_datetime(dt, 'H:mm zzzz', tzinfo=british, locale='en_US') + u'16:30 British Summer Time' + +Here, the given UTC time is adjusted to the "Europe/London" time-zone, and +daylight savings time is taken into account. Daylight savings time is also +applied to ``format_time``, but because the actual date is unknown in that +case, the current day is assumed to determine whether DST or standard time +should be used. + + .. _`pytz`: http://pytz.sourceforge.net/ + + +Parsing Dates +============= + +Babel can also parse date and time information in a locale-sensitive manner: + +.. code-block:: pycon + + >>> from babel.dates import parse_date, parse_datetime, parse_time
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/display.txt @@ -0,0 +1,83 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +==================== +Locale Display Names +==================== + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +Introduction +============ + +While `message catalogs <messages.html>`_ allow you to localize any messages +in your application, there are a number of strings that are used in many +applications for which translations are readily available. + +Imagine for example you have a list of countries that users can choose from, +and you'd like to display the names of those countries in the language the +user prefers. Instead of translating all those country names yourself in your +application, you can make use of the translations provided by the locale data +included with Babel, which is based on the `Common Locale Data Repository +(CLDR) <http://unicode.org/cldr/>`_ developed and maintained by the `Unicode +Consortium <http://unicode.org/>`_. + + +The ``Locale`` Class +==================== + +You normally access such locale data through the `Locale`_ class provided +by Babel: + +.. code-block:: pycon + + >>> from babel import Locale + >>> locale = Locale('en', 'US') + >>> locale.territories['US'] + u'United States' + >>> locale = Locale('es', 'MX') + >>> locale.territories['US'] + u'Estados Unidos' + +.. _`Locale`: api/babel.core.Locale-class.html + +In addition to country/territory names, the locale data also provides access to +names of languages, scripts, variants, time zones, and more. Some of the data +is closely related to number and date formatting. + +Most of the corresponding ``Locale`` properties return dictionaries, where the +key is a code such as the ISO country and language codes. Consult the API +documentation for references to the relevant specifications. + + +Calender Display Names +====================== + +The `Locale`_ class provides access to many locale display names related to +calendar display, such as the names of week days or months. + +These display names are of course used for date formatting, but can also be +used, for example, to show a list of months to the user in their preferred +language: + +.. code-block:: pycon + + >>> locale = Locale('es') + >>> month_names = locale.months['format']['wide'].items() + >>> month_names.sort() + >>> for idx, name in month_names: + ... print name + enero + febrero + marzo + abril + mayo + junio + julio + agosto + septiembre + octubre + noviembre + diciembre
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/index.txt @@ -0,0 +1,30 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +===== +Babel +===== + +.. image:: logo.png + :width: 426 + :height: 187 + :align: center + :alt: Babel + :class: logo + +--------------------------------------------------- +Simple Internationalization for Python Applications +--------------------------------------------------- + +Babel is an integrated collection of utilities that assist in +internationalizing and localizing Python applications, with an emphasis on +web-based applications. + + * `Introduction <intro.html>`_ + * `Locale Display Names <display.html>`_ + * `Date Formatting <dates.html>`_ + * `Number Formatting <numbers.html>`_ + * `Working with Message Catalogs <messages.html>`_ + * `Command-Line Interface <cmdline.html>`_ + * `Distutils/Setuptools Integration <setup.html>`_ + * `Support Classes and Functions <support.html>`_ + * `Generated API Documentation <api/index.html>`_
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/intro.txt @@ -0,0 +1,61 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +============ +Introduction +============ + +The functionality Babel provides for internationalization (I18n) and +localization (L10N) can be separated into two different aspects: + + * tools to build and work with ``gettext`` message catalogs, and + * a Python interface to the CLDR (Common Locale Data Repository), providing + access to various locale display names, localized number and date + formatting, etc. + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +Message Catalogs +================ + +While the Python standard library includes a +`gettext <http://docs.python.org/lib/module-gettext.html>`_ module that enables +applications to use message catalogs, it requires developers to build these +catalogs using GNU tools such as ``xgettext``, ``msgmerge``, and ``msgfmt``. +And while ``xgettext`` does have support for extracting messages from Python +files, it does not know how to deal with other kinds of files commonly found +in Python web-applications, such as templates, nor does it provide an easy +extensibility mechanism to add such support. + +Babel addresses this by providing a framework where various extraction methods +can be plugged in to a larger message extraction framework, and also removes +the dependency on the GNU ``gettext`` tools for common tasks, as these aren't +necessarily available on all platforms. See `Working with Message Catalogs`_ +for details on this aspect of Babel. + +.. _`Working with Message Catalogs`: messages.html + + +Locale Data +=========== + +Furthermore, while the Python standard library does include support for basic +localization with respect to the formatting of numbers and dates (the +`locale <http://docs.python.org/lib/module-locale.html>`_ module, among others), +this support is based on the assumption that there will be only one specific +locale used per process (at least simultaneously.) Also, it doesn't provide +access to other kinds of locale data, such as the localized names of countries, +languages, or time-zones, which are frequently needed in web-based applications. + +For these requirements, Babel includes data extracted from the `Common Locale +Data Repository (CLDR) <http://unicode.org/cldr/>`_, and provides a number of +convenient methods for accessing and using this data. See `Locale Display +Names`_, `Date Formatting`_, and `Number Formatting`_ for more information on +this aspect of Babel. + + +.. _`Locale Display Names`: display.html +.. _`Date Formatting`: dates.html +.. _`Number Formatting`: numbers.html
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5385fd067c5f2433249ee2445e466a4ee2274746 GIT binary patch literal 44986 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�s2g$=NFE7wv1FYXt@Bx#Pv<Qouv_0nT{@xX{kyg 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
new file mode 100755 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8060875c307123012418676deef8e0f8f3a2f327 GIT binary patch literal 15484 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
new file mode 100755 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b9eddbdd9b831a21f0a0d733d3d4f31614d34439 GIT binary patch literal 6118 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 G$o~RM%YoAX
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/messages.txt @@ -0,0 +1,276 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +============================= +Working with Message Catalogs +============================= + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +Introduction +============ + +The ``gettext`` translation system enables you to mark any strings used in your +application as subject to localization, by wrapping them in functions such as +``gettext(str)`` and ``ngettext(singular, plural, num)``. For brevity, the +``gettext`` function is often aliased to ``_(str)``, so you can write: + +.. code-block:: python + + print _("Hello") + +instead of just: + +.. code-block:: python + + print "Hello" + +to make the string "Hello" localizable. + +Message catalogs are collections of translations for such localizable messages +used in an application. They are commonly stored in PO (Portable Object) and MO +(Machine Object) files, the formats of which are defined by the GNU `gettext`_ +tools and the GNU `translation project`_. + + .. _`gettext`: http://www.gnu.org/software/gettext/ + .. _`translation project`: http://sourceforge.net/projects/translation + +The general procedure for building message catalogs looks something like this: + + * use a tool (such as ``xgettext``) to extract localizable strings from the + code base and write them to a POT (PO Template) file. + * make a copy of the POT file for a specific locale (for example, "en_US") + and start translating the messages + * use a tool such as ``msgfmt`` to compile the locale PO file into an binary + MO file + * later, when code changes make it necessary to update the translations, you + regenerate the POT file and merge the changes into the various + locale-specific PO files, for example using ``msgmerge`` + +Python provides the `gettext module`_ as part of the standard library, which +enables applications to work with appropriately generated MO files. + + .. _`gettext module`: http://docs.python.org/lib/module-gettext.html + +As ``gettext`` provides a solid and well supported foundation for translating +application messages, Babel does not reinvent the wheel, but rather reuses this +infrastructure, and makes it easier to build message catalogs for Python +applications. + + +Message Extraction +================== + +Babel provides functionality similar to that of the ``xgettext`` program, +except that only extraction from Python source files is built-in, while support +for other file formats can be added using a simple extension mechanism. + +Unlike ``xgettext``, which is usually invoked once for every file, the routines +for message extraction in Babel operate on directories. While the per-file +approach of ``xgettext`` works nicely with projects using a ``Makefile``, +Python projects rarely use ``make``, and thus a different mechanism is needed +for extracting messages from the heterogeneous collection of source files that +many Python projects are composed of. + +When message extraction is based on directories instead of individual files, +there needs to be a way to configure which files should be treated in which +manner. For example, while many projects may contain ``.html`` files, some of +those files may be static HTML files that don't contain localizable message, +while others may be `Django`_ templates, and still others may contain `Genshi`_ +markup templates. Some projects may even mix HTML files for different templates +languages (for whatever reason). Therefore the way in which messages are +extracted from source files can not only depend on the file extension, but +needs to be controllable in a precise manner. + +.. _`Django`: http://www.djangoproject.com/ +.. _`Genshi`: http://genshi.edgewall.org/ + +Babel accepts a configuration file to specify this mapping of files to +extraction methods, which is described below. + + +.. _`mapping`: + +------------------------------------------- +Extraction Method Mapping and Configuration +------------------------------------------- + +The mapping of extraction methods to files in Babel is done via a configuration +file. This file maps extended glob patterns to the names of the extraction +methods, and can also set various options for each pattern (which options are +available depends on the specific extraction method). + +For example, the following configuration adds extraction of messages from both +Genshi markup templates and text templates: + +.. code-block:: ini + + # Extraction from Python source files + + [python: foobar/**.py] + + # Extraction from Genshi HTML and text templates + + [genshi: foobar/**/templates/**.html] + ignore_tags = script,style + include_attrs = alt title summary + + [genshi: foobar/**/templates/**.txt] + template_class = genshi.template.text:TextTemplate + encoding = ISO-8819-15 + +The configuration file syntax is based on the format commonly found in ``.INI`` +files on Windows systems, and as supported by the ``ConfigParser`` module in +the Python standard libraries. Section names (the strings enclosed in square +brackets) specify both the name of the extraction method, and the extended glob +pattern to specify the files that this extraction method should be used for, +separated by a colon. The options in the sections are passed to the extraction +method. Which options are available is specific to the extraction method used. + +The extended glob patterns used in this configuration are similar to the glob +patterns provided by most shells. A single asterisk (``*``) is a wildcard for +any number of characters (except for the pathname component separator "/"), +while a question mark (``?``) only matches a single character. In addition, +two subsequent asterisk characters (``**``) can be used to make the wildcard +match any directory level, so the pattern ``**.txt`` matches any file with the +extension ``.txt`` in any directory. + +Lines that start with a ``#`` or ``;`` character are ignored and can be used +for comments. Empty lines are also ignored, too. + +.. note:: if you're performing message extraction using the command Babel + provides for integration into ``setup.py`` scripts (see below), you + can also provide this configuration in a different way, namely as a + keyword argument to the ``setup()`` function. + + +---------- +Front-Ends +---------- + +Babel provides two different front-ends to access its functionality for working +with message catalogs: + + * A `Command-line interface <cmdline.html>`_, and + * `Integration with distutils/setuptools <setup.html>`_ + +Which one you choose depends on the nature of your project. For most modern +Python projects, the distutils/setuptools integration is probably more +convenient. + + +-------------------------- +Writing Extraction Methods +-------------------------- + +Adding new methods for extracting localizable methods is easy. First, you'll +need to implement a function that complies with the following interface: + +.. code-block:: python + + def extract_xxx(fileobj, keywords, comment_tags, options): + """Extract messages from XXX files. + + :param fileobj: the file-like object the messages should be extracted + from + :param keywords: a list of keywords (i.e. function names) that should + be recognized as translation functions + :param comment_tags: a list of translator tags to search for and + include in the results + :param options: a dictionary of additional options (optional) + :return: an iterator over ``(lineno, funcname, message, comments)`` + tuples + :rtype: ``iterator`` + """ + +.. note:: Any strings in the tuples produced by this function must be either + ``unicode`` objects, or ``str`` objects using plain ASCII characters. + That means that if sources contain strings using other encodings, it + is the job of the extractor implementation to do the decoding to + ``unicode`` objects. + +Next, you should register that function as an entry point. This requires your +``setup.py`` script to use `setuptools`_, and your package to be installed with +the necessary metadata. If that's taken care of, add something like the +following to your ``setup.py`` script: + +.. code-block:: python + + def setup(... + + entry_points = """ + [babel.extractors] + xxx = your.package:extract_xxx + """, + +That is, add your extraction method to the entry point group +``babel.extractors``, where the name of the entry point is the name that people +will use to reference the extraction method, and the value being the module and +the name of the function (separated by a colon) implementing the actual +extraction. + +.. _`setuptools`: http://peak.telecommunity.com/DevCenter/setuptools + +Comments Tags And Translator Comments Explanation +................................................. + +First of all what are comments tags. Comments tags are excerpts of text to +search for in comments, only comments, right before the `python gettext`_ +calls, as shown on the following example: + + .. _`python gettext`: http://docs.python.org/lib/module-gettext.html + +.. code-block:: python + + # NOTE: This is a comment about `Foo Bar` + _('Foo Bar') + +The comments tag for the above example would be ``NOTE:``, and the translator +comment for that tag would be ``This is a comment about `Foo Bar```. + +The resulting output in the catalog template would be something like:: + + #. This is a comment about `Foo Bar` + #: main.py:2 + msgid "Foo Bar" + msgstr "" + +Now, you might ask, why would I need that? + +Consider this simple case; you have a menu item called “Manual”. You know what +it means, but when the translator sees this they will wonder did you mean: + +1. a document or help manual, or +2. a manual process? + +This is the simplest case where a translation comment such as +“The installation manual” helps to clarify the situation and makes a translator +more productive. + +**More examples of the need for translation comments** + +Real world examples are best. This is a discussion over the use of the word +“Forward” in Northern Sotho: + +“When you go forward. You go ‘Pele’, but when you forward the document, +you ‘Fetišetša pele’. So if you just say forward, we don’t know what you are +talking about. +It is better if it's in a sentence. But in this case i think we will use ‘pele’ +because on the string no. 86 and 88 there is “show previous page in history” +and “show next page in history”. + +Were the translators guess correct? I think so, but it makes it so much easier +if they don’t need to be super `sleuths`_ as well as translators. + + .. _`sleuths`: http://www.thefreedictionary.com/sleuth + + +*Explanation Borrowed From:* `Wordforge`_ + + .. _`Wordforge`: http://www.wordforge.org/static/translation_comments.html + +**Note**: Translator comments are currently only supported in python source +code. +
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/numbers.txt @@ -0,0 +1,111 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +========================== +Number Formatting +========================== + + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +Support for locale-specific formatting and parsing of numbers is provided by +the ``babel.numbers`` module: + +.. code-block:: pycon + + >>> from babel.numbers import format_number, format_decimal, format_percent + +Examples: + +.. code-block:: pycon + + >>> format_decimal(1.2345, locale='en_US') + u'1.234' + >>> format_decimal(1.2345, locale='sv_SE') + u'1,234' + >>> format_decimal(12345, locale='de_DE') + u'12.345' + + +Pattern Syntax +============== + +While Babel makes it simple to use the appropriate number format for a given +locale, you can also force it to use custom patterns. As with date/time +formatting patterns, the patterns Babel supports for number formatting are +based on the `Locale Data Markup Language specification`_ (LDML). + +Examples: + +.. code-block:: pycon + + >>> format_decimal(-1.2345, format='#,##0.##;-#', locale='en') + u'-1.23' + >>> format_decimal(-1.2345, format='#,##0.##;(#)', locale='en') + u'(1.23)' + +The syntax for custom number format patterns is described in detail in the +the specification. The following table is just a relatively brief overview. + + .. _`Locale Data Markup Language specification`: http://unicode.org/reports/tr35/#Number_Format_Patterns + + +----------+-----------------------------------------------------------------+ + | Symbol | Description | + +==========+=================================================================+ + | ``0`` | Digit | + +----------+-----------------------------------------------------------------+ + | ``1-9`` | '1' through '9' indicate rounding. | + +----------+-----------------------------------------------------------------+ + | ``@`` | Significant digit | + +----------+-----------------------------------------------------------------+ + | ``#`` | Digit, zero shows as absent | + +----------+-----------------------------------------------------------------+ + | ``.`` | Decimal separator or monetary decimal separator | + +----------+-----------------------------------------------------------------+ + | ``-`` | Minus sign | + +----------+-----------------------------------------------------------------+ + | ``,`` | Grouping separator | + +----------+-----------------------------------------------------------------+ + | ``E`` | Separates mantissa and exponent in scientific notation | + +----------+-----------------------------------------------------------------+ + | ``+`` | Prefix positive exponents with localized plus sign | + +----------+-----------------------------------------------------------------+ + | ``;`` | Separates positive and negative subpatterns | + +----------+-----------------------------------------------------------------+ + | ``%`` | Multiply by 100 and show as percentage | + +----------+-----------------------------------------------------------------+ + | ``‰`` | Multiply by 1000 and show as per mille | + +----------+-----------------------------------------------------------------+ + | ``¤`` | Currency sign, replaced by currency symbol. If doubled, | + | | replaced by international currency symbol. If tripled, uses the | + | | long form of the decimal symbol. | + +----------+-----------------------------------------------------------------+ + | ``'`` | Used to quote special characters in a prefix or suffix | + +----------+-----------------------------------------------------------------+ + | ``*`` | Pad escape, precedes pad character | + +----------+-----------------------------------------------------------------+ + + +Parsing Numbers +=============== + +Babel can also parse numeric data in a locale-sensitive manner: + +.. code-block:: pycon + + >>> from babel.numbers import parse_decimal, parse_number + +Examples: + +.. code-block:: pycon + + >>> parse_decimal('1,099.98', locale='en_US') + 1099.98 + >>> parse_decimal('1.099,98', locale='de') + 1099.98 + >>> parse_decimal('2,109,998', locale='de') + Traceback (most recent call last): + ... + NumberFormatError: '2,109,998' is not a valid decimal number
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/setup.txt @@ -0,0 +1,214 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +================================ +Distutils/Setuptools Integration +================================ + +Babel provides commands for integration into ``setup.py`` scripts, based on +either the ``distutils`` package that is part of the Python standard library, +or the third-party ``setuptools`` package. + +These commands are available by default when Babel has been properly installed, +and ``setup.py`` is using ``setuptools``. For projects that use plain old +``distutils``, the commands need to be registered explicitly, for example: + +.. code-block:: python + + from distutils.core import setup + from babel.messages import frontend as babel + + setup( + ... + cmd_class = {'extract_messages': babel.extract_messages, + 'new_catalog': babel.new_catalog} + ) + + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +extract_messages +================ + +The ``extract_messages`` command is comparable to the GNU ``xgettext`` program: +it can extract localizable messages from a variety of difference source files, +and generate a PO (portable object) template file from the collected messages. + +If the command has been correctly installed or registered, another project's +``setup.py`` script should allow you to use the command:: + + $ ./setup.py extract_messages --help + Global options: + --verbose (-v) run verbosely (default) + --quiet (-q) run quietly (turns verbosity off) + --dry-run (-n) don't actually do anything + --help (-h) show detailed help message + + Options for 'extract_messages' command: + ... + +Running the command will produce a PO template file:: + + $ ./setup.py extract_messages --output-file foobar/locale/messages.pot + running extract_messages + extracting messages from foobar/__init__.py + extracting messages from foobar/core.py + ... + writing PO template file to foobar/locale/messages.pot + + +Method Mapping +-------------- + +The mapping of file patterns to extraction methods (and options) can be +specified using a configuration file that is pointed to using the +``--mapping-file`` option shown above. Alternatively, you can configure the +mapping directly in ``setup.py`` using a keyword argument to the ``setup()`` +function: + +.. code-block:: python + + setup(... + + message_extractors = { + 'foobar': [ + ('**.py', 'python', None), + ('**/templates/**.html', 'genshi', None), + ('**/templates/**.txt', 'genshi', { + 'template_class': 'genshi.template.text.TextTemplate' + }) + ], + }, + + ... + ) + + +Options +------- + +The ``extract_messages`` command accepts the following options: + + +-----------------------------+----------------------------------------------+ + | Option | Description | + +=============================+==============================================+ + | ``--charset`` | charset to use in the output file | + +-----------------------------+----------------------------------------------+ + | ``--keywords`` (``-k``) | space-separated list of keywords to look for | + | | in addition to the defaults | + +-----------------------------+----------------------------------------------+ + | ``--no-default-keywords`` | do not include the default keywords | + +-----------------------------+----------------------------------------------+ + | ``--mapping-file`` (``-F``) | path to the mapping configuration file | + +-----------------------------+----------------------------------------------+ + | ``--no-location`` | do not include location comments with | + | | filename and line number | + +-----------------------------+----------------------------------------------+ + | ``--omit-header`` | do not include msgid "" entry in header | + +-----------------------------+----------------------------------------------+ + | ``--output-file`` (``-o``) | name of the output file | + +-----------------------------+----------------------------------------------+ + | ``--width`` (``-w``) | set output line width (default 76) | + +-----------------------------+----------------------------------------------+ + | ``--no-wrap`` | do not break long message lines, longer than | + | | the output line width, into several lines | + +-----------------------------+----------------------------------------------+ + | ``--input-dirs`` | directories that should be scanned for | + | | messages | + +-----------------------------+----------------------------------------------+ + | ``--sort-output`` | generate sorted output (default False) | + +-----------------------------+----------------------------------------------+ + | ``--sort-by-file`` | sort output by file location (default False) | + +-----------------------------+----------------------------------------------+ + | ``--msgid-bugs-address`` | set email address for message bug reports | + +-----------------------------+----------------------------------------------+ + | ``--copyright-holder`` | set copyright holder in output | + +-----------------------------+----------------------------------------------+ + | ``--add-comments (-c)`` | place comment block with TAG (or those | + | | preceding keyword lines) in output file. | + | | Separate multiple TAGs with commas(,) | + +-----------------------------+----------------------------------------------+ + +These options can either be specified on the command-line, or in the +``setup.cfg`` file. In the latter case, the options above become entries of the +section ``[extract_messages]``, and the option names are changed to use +underscore characters instead of dashes, for example: + +.. code-block:: ini + + [extract_messages] + keywords = _, gettext, ngettext + mapping_file = babel.cfg + width = 80 + +This would be equivalent to invoking the command from the command-line as +follows:: + + $ setup.py extract_messages -k _ -k gettext -k ngettext -F mapping.cfg -w 80 + +Any path names are interpreted relative to the location of the ``setup.py`` +file. For boolean options, use "true" or "false" values. + + +new_catalog +=========== + +The ``new_catalog`` command is basically equivalent to the GNU ``msginit`` +program: it creates a new translation catalog based on a PO template file (POT). + +If the command has been correctly installed or registered, another project's +``setup.py`` script should allow you to use the command:: + + $ ./setup.py new_catalog --help + Global options: + --verbose (-v) run verbosely (default) + --quiet (-q) run quietly (turns verbosity off) + --dry-run (-n) don't actually do anything + --help (-h) show detailed help message + + Options for 'new_catalog' command: + ... + +Running the command will produce a PO file:: + + $ ./setup.py new_catalog -l fr -i foobar/locales/messages.pot \ + -o foobar/locales/fr/messages.po + running new_catalog + creating catalog 'foobar/locales/fr/messages.po' based on 'foobar/locales/messages.pot' + + +Options +------- + +The ``new_catalog`` command accepts the following options: + + +-----------------------------+----------------------------------------------+ + | Option | Description | + +=============================+==============================================+ + | ``--domain`` | domain of the PO file (defaults to | + | | lower-cased project name) | + +-----------------------------+----------------------------------------------+ + | ``--input-file`` (``-i``) | name of the input file | + +-----------------------------+----------------------------------------------+ + | ``--output-dir`` (``-d``) | name of the output directory | + +-----------------------------+----------------------------------------------+ + | ``--output-file`` (``-o``) | name of the output file | + +-----------------------------+----------------------------------------------+ + | ``--locale`` | locale for the new localized string | + +-----------------------------+----------------------------------------------+ + | ``--omit-header`` | do not include msgid "" entry in header | + +-----------------------------+----------------------------------------------+ + | ``--first-author`` | name of the first author | + +-----------------------------+----------------------------------------------+ + | ``--first-author-email`` | email address of the first author | + +-----------------------------+----------------------------------------------+ + +If ``output-dir`` is specified, but ``output-file`` is not, the default filename +of the output file will be:: + + <output_dir>/<locale>/LC_MESSAGES/<domain>.po + +These options can either be specified on the command-line, or in the +``setup.cfg`` file.
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..90e92682135d3f7213332f870f973bd06d6d57ee GIT binary patch literal 112 zc%17D@N?(olHy`uVBq!ia0vp^B0wy_$P6TPuAPklQfvV}A+FxOzM<ja$?1EZ14UFl zT^vI=t|to=iYB~>SaRTj#)AV3j$Ayz@Myi$<MK684X4~sFf(*H@+4o{pJfBoz~JfX K=d#Wzp$PzDHzRKV
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/style/docutils.css @@ -0,0 +1,277 @@ +/* +:Author: David Goodger +:Contact: goodger@users.sourceforge.net +:Date: $Date: 2005-12-18 01:56:14 +0100 (Sun, 18 Dec 2005) $ +:Revision: $Revision: 4224 $ +:Copyright: This stylesheet has been placed in the public domain. + +Default cascading style sheet for the HTML output of Docutils. + +See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to +customize this style sheet. +*/ + +/* used to remove borders from tables and images */ +.borderless, table.borderless td, table.borderless th { + border: 0 } + +table.borderless td, table.borderless th { + /* Override padding for "table.docutils td" with "! important". + The right padding separates the table cells. */ + padding: 0 0.5em 0 0 ! important } + +.first { + /* Override more specific margin styles with "! important". */ + margin-top: 0 ! important } + +.last, .with-subtitle { + margin-bottom: 0 ! important } + +.hidden { + display: none } + +a.toc-backref { + text-decoration: none ; + color: black } + +blockquote.epigraph { + margin: 2em 5em ; } + +dl.docutils dd { + margin-bottom: 0.5em } + +dl.docutils dt { + font-weight: bold } + +div.abstract { + margin: 2em 5em } + +div.abstract p.topic-title { + font-weight: bold ; + text-align: center } + +div.admonition, div.attention, div.caution, div.danger, div.error, +div.hint, div.important, div.note, div.tip, div.warning { + margin: 2em ; + border: medium outset ; + padding: 1em } + +div.admonition p.admonition-title, div.hint p.admonition-title, +div.important p.admonition-title, div.note p.admonition-title, +div.tip p.admonition-title { + font-weight: bold ; + font-family: sans-serif } + +div.attention p.admonition-title, div.caution p.admonition-title, +div.danger p.admonition-title, div.error p.admonition-title, +div.warning p.admonition-title { + color: red ; + font-weight: bold ; + font-family: sans-serif } + +/* Uncomment (and remove this text!) to get reduced vertical space in + compound paragraphs. +div.compound .compound-first, div.compound .compound-middle { + margin-bottom: 0.5em } + +div.compound .compound-last, div.compound .compound-middle { + margin-top: 0.5em } +*/ + +div.dedication { + margin: 2em 5em ; + text-align: center ; + font-style: italic } + +div.dedication p.topic-title { + font-weight: bold ; + font-style: normal } + +div.figure { + margin-left: 2em ; + margin-right: 2em } + +div.footer, div.header { + clear: both; + font-size: smaller } + +div.line-block { + display: block ; + margin-top: 1em ; + margin-bottom: 1em } + +div.line-block div.line-block { + margin-top: 0 ; + margin-bottom: 0 ; + margin-left: 1.5em } + +div.sidebar { + margin-left: 1em ; + border: medium outset ; + padding: 1em ; + background-color: #ffffee ; + width: 40% ; + float: right ; + clear: right } + +div.sidebar p.rubric { + font-family: sans-serif ; + font-size: medium } + +div.system-messages { + margin: 5em } + +div.system-messages h1 { + color: red } + +div.system-message { + border: medium outset ; + padding: 1em } + +div.system-message p.system-message-title { + color: red ; + font-weight: bold } + +div.topic { + margin: 2em } + +h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, +h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { + margin-top: 0.4em } + +h1.title { + text-align: center } + +h2.subtitle { + text-align: center } + +hr.docutils { + width: 75% } + +img.align-left { + clear: left } + +img.align-right { + clear: right } + +ol.simple, ul.simple { + margin-bottom: 1em } + +ol.arabic { + list-style: decimal } + +ol.loweralpha { + list-style: lower-alpha } + +ol.upperalpha { + list-style: upper-alpha } + +ol.lowerroman { + list-style: lower-roman } + +ol.upperroman { + list-style: upper-roman } + +p.attribution { + text-align: right ; + margin-left: 50% } + +p.caption { + font-style: italic } + +p.credits { + font-style: italic ; + font-size: smaller } + +p.label { + white-space: nowrap } + +p.rubric { + font-weight: bold ; + font-size: larger ; + color: maroon ; + text-align: center } + +p.sidebar-title { + font-family: sans-serif ; + font-weight: bold ; + font-size: larger } + +p.sidebar-subtitle { + font-family: sans-serif ; + font-weight: bold } + +p.topic-title { + font-weight: bold } + +pre.address { + margin-bottom: 0 ; + margin-top: 0 ; + font-family: serif ; + font-size: 100% } + +pre.literal-block, pre.doctest-block { + margin-left: 2em ; + margin-right: 2em ; + background-color: #eeeeee } + +span.classifier { + font-family: sans-serif ; + font-style: oblique } + +span.classifier-delimiter { + font-family: sans-serif ; + font-weight: bold } + +span.interpreted { + font-family: sans-serif } + +span.option { + white-space: nowrap } + +span.pre { + white-space: pre } + +span.problematic { + color: red } + +span.section-subtitle { + /* font-size relative to parent (h1..h6 element) */ + font-size: 80% } + +table.citation { + border-left: solid 1px gray; + margin-left: 1px } + +table.docinfo { + margin: 2em 4em } + +table.docutils { + margin-top: 0.5em ; + margin-bottom: 0.5em } + +table.footnote { + border-left: solid 1px black; + margin-left: 1px } + +table.docutils td, table.docutils th, +table.docinfo td, table.docinfo th { + padding-left: 0.5em ; + padding-right: 0.5em ; + vertical-align: top } + +table.docutils th.field-name, table.docinfo th.docinfo-name { + font-weight: bold ; + text-align: left ; + white-space: nowrap ; + padding-left: 0 } + +h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, +h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { + font-size: 100% } + +tt.docutils { + background-color: #eeeeee } + +ul.auto-toc { + list-style-type: none }
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/style/edgewall.css @@ -0,0 +1,77 @@ +@import url(docutils.css); +@import url(pygments.css); + +html, body { height: 100%; margin: 0; padding: 0; } +html, body { background: #4b4d4d url(bkgnd_pattern.png); color: #000; } +body, th, td { + font: normal small Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif; +} +pre, code, tt { font-size: medium; } +h1, h2, h3, h4 { + border-bottom: 1px solid #ccc; + font-family: Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif; + font-weight: bold; letter-spacing: -0.018em; +} +h1 { font-size: 19px; margin: 2em 0 .5em; } +h2 { font-size: 16px; margin: 1.5em 0 .5em; } +h3 { font-size: 14px; margin: 1.2em 0 .5em; } +hr { border: none; border-top: 1px solid #ccb; margin: 2em 0; } +p { margin: 0 0 1em; } + +table { border: 1px solid #999; border-width: 0 1px 0 0; + border-collapse: separate; border-spacing: 0; +} +table thead th { background: #999; border: 1px solid #999;; color: #fff; + font-weight: bold; +} +table td { border: 1px solid #ccc; border-width: 0 0 1px 1px; padding: .3em; } + +:link, :visited { text-decoration: none; border-bottom: 1px dotted #bbb; + color: #b00; +} +:link:hover, :visited:hover { background-color: #eee; color: #555; } +:link img, :visited img { border: none } +h1 :link, h1 :visited ,h2 :link, h2 :visited, h3 :link, h3 :visited, +h4 :link, h4 :visited, h5 :link, h5 :visited, h6 :link, h6 :visited { + color: #000; +} + +div.document { background: #fff url(shadow.gif) right top repeat-y; + border-left: 1px solid #000; margin: 0 auto 0 40px; + min-height: 100%; width: 54em; padding: 0 180px 1px 20px; +} +h1.title, div.document#babel h1 { border: none; color: #666; + font-size: x-large; margin: 0 -20px 1em; padding: 2em 20px 0; +} +h1.title { background: url(vertbars.png) repeat-x; } +div.document#babel h1.title { text-indent: -4000px; } +div.document#babel h1 { text-align: center; } +pre.literal-block, div.highlight pre { background: #f4f4f4; + border: 1px solid #e6e6e6; color: #000; margin: 1em 1em; padding: .25em; + overflow: auto; +} + +div.contents { font-size: 90%; position: absolute; position: fixed; + margin-left: 80px; left: 60em; top: 30px; right: 0; +} +div.contents .topic-title { display: none; } +div.contents ul { list-style: none; padding-left: 0; } +div.contents :link, div.contents :visited { color: #c6c6c6; border: none; + display: block; padding: 3px 5px 3px 10px; +} +div.contents :link:hover, div.contents :visited:hover { background: #000; + color: #fff; +} + +div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, +div.important, div.note, div.tip, div.warning { + border: none; color: #333; font-style: italic; margin: 1em 2em; +} +div.attention p.admonition-title, div.caution p.admonition-title, div.danger +p.admonition-title, div.error p.admonition-title, +div.warning p.admonition-title { + color: #b00; +} +p.admonition-title { margin-bottom: 0; text-transform: uppercase; } +tt.docutils { background-color: transparent; } +span.pre { white-space: normal; }
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/style/epydoc.css @@ -0,0 +1,136 @@ +html { background: #4b4d4d url(../style/bkgnd_pattern.png); margin: 0; + padding: 1em 1em 3em; +} +body { background: #fff url(../style/vertbars.png) repeat-x; + border: 1px solid #000; color: #000; margin: 1em 0; padding: 0 1em 1em; +} +body, th, td { + font: normal small Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif; +} +h1, h2, h3, h4 { + font-family: Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif; + font-weight: bold; letter-spacing: -0.018em; +} +h1 { font-size: 19px; margin: 2em 0 .5em; } +h2 { font-size: 16px; margin: 1.5em 0 .5em; } +h3 { font-size: 14px; margin: 1.2em 0 .5em; } +hr { border: none; border-top: 1px solid #ccb; margin: 2em 0; } +p { margin: 0 0 1em; } +:link, :visited { text-decoration: none; border-bottom: 1px dotted #bbb; + color: #b00; +} +:link:hover, :visited:hover { background-color: #eee; color: #555; } + +table { border: none; border-collapse: collapse; } + +table.navbar { background: #000; color: #fff; margin: 2em 0 .33em; } +table.navbar th { border: 1px solid #000; font-weight: bold; padding: 1px; } +table.navbar :link, table.navbar :visited { border: none; color: #fff; } +table.navbar :link:hover, table.navbar :visited:hover { background: none; + text-decoration: underline overline; +} +table.navbar th.navbar-select { background: #fff; color: #000; } +span.breadcrumbs { color: #666; font-size: 95%; } +h1.epydoc { border: none; color: #666; + font-size: x-large; margin: 1em 0 0; padding: 0; +} +pre.base-tree { color: #666; margin: 0; padding: 0; } +pre.base-tree :link, pre.base-tree :visited { border: none; } +pre.py-doctest, pre.variable, pre.rst-literal-block { background: #eee; + border: 1px solid #e6e6e6; color: #000; margin: 1em; padding: .25em; + overflow: auto; +} +pre.variable { margin: 0; } + +/* Summary tables */ + +table.summary { margin: .5em 0; } +table.summary tr.table-header { background: #f7f7f0; } +table.summary td.table-header { color: #666; font-weight: bold; } +table.summary th.group-header { background: #f7f7f0; color: #666; + font-size: 90%; font-weight: bold; text-align: left; +} +table.summary th, table.summary td { border: 1px solid #d7d7d7; } +table.summary th th, table.summary td td { border: none; } +table.summary td.summary table td { color: #666; font-size: 90%; } +table.summary td.summary table br { display: none; } +table.summary td.summary span.summary-type { font-family: monospace; + font-size: 90%; +} +table.summary td.summary span.summary-type code { font-size: 110%; } +p.indent-wrapped-lines { color: #999; font-size: 85%; margin: 0; + padding: 0 0 0 7em; text-indent: -7em; +} +p.indent-wrapped-lines code { color: #999; font-size: 115%; } +p.indent-wrapped-lines :link, p.indent-wrapped-lines :visited { border: none; } +.summary-sig { display: block; font-family: monospace; font-size: 120%; + margin-bottom: .5em; +} +.summary-sig-name { font-weight: bold; } +.summary-sig-arg { color: #333; } +.summary-sig :link, .summary-sig :visited { border: none; } +.summary-name { font-family: monospace; font-weight: bold; } + +/* Details tables */ + +table.details { margin: 2em 0 0; } +div table.details { margin-top: 0; } +table.details tr.table-header { background: transparent; } +table.details td.table-header { border-bottom: 1px solid #ccc; padding: 2em 0 0; } +table.details span.table-header { + font: bold 140% Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif; + letter-spacing: -0.018em; +} +table.details th, table.details td { border: none; } +table.details th th, table.details td td { border: none; } +table.details td { padding-left: 2em; } +table.details td td { padding-left: 0; } +table.details h3.epydoc { margin-left: -2em; } +table.details h3.epydoc .sig { color: #999; font-family: monospace; } +table.details h3.epydoc .sig-name { color: #000; } +table.details h3.epydoc .sig-arg { color: #666; } +table.details h3.epydoc .sig-default { font-size: 95%; font-weight: normal; } +table.details h3.epydoc .sig-default code { font-weight: normal; } +table.details h3.epydoc .fname { color: #999; font-size: 90%; + font-style: italic; font-weight: normal; line-height: 1.6em; +} + +dl dt { color: #666; margin-top: 1em; } +dl dd { margin: 0; padding-left: 2em; } +dl.fields { margin: 1em 0; padding: 0; } +dl.fields dt { color: #666; margin-top: 1em; } +dl.fields dd ul { margin: 0; padding: 0; } +div.fields { font-size: 90%; margin: 0 0 2em 2em; } +div.fields p { margin-bottom: 0.5em; } + +table td.footer { color: #999; font-size: 85%; margin-top: 3em; + padding: 0 3em 1em; position: absolute; width: 80%; } +table td.footer :link, table td.footer :visited { border: none; color: #999; } +table td.footer :link:hover, table td.footer :visited:hover { + background: transparent; text-decoration: underline; +} + +/* Syntax highlighting */ + +.py-prompt, .py-more, .variable-ellipsis, .variable-op { color: #999; } +.variable-group { color: #666; font-weight: bold; } +.py-string, .variable-string, .variable-quote { color: #093; } +.py-comment { color: #06f; font-style: italic; } +.py-keyword { color: #00f; } +.py-output { background: #f6f6f0; color: #666; font-weight: bold; } + +/* Index */ + +table.link-index { background: #f6f6f0; border: none; margin-top: 1em; } +table.link-index td.link-index { border: none; font-family: monospace; + font-weight: bold; padding: .5em 1em; +} +table.link-index td table, table.link-index td td { border: none; } +table.link-index .index-where { color: #999; + font-family: Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif; + font-size: 90%; font-weight: normal; line-height: 1.6em; +} +table.link-index .index-where :link, table.link-index .index-where :visited { + border: none; color: #666; +} +h2.epydoc { color: #999; font-size: 200%; line-height: 10px; }
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/style/pygments.css @@ -0,0 +1,57 @@ +div.highlight { background: #ffffff; } +div.highlight .c { color: #999988; font-style: italic } +div.highlight .err { color: #a61717; background-color: #e3d2d2 } +div.highlight .k { font-weight: bold } +div.highlight .o { font-weight: bold } +div.highlight .cm { color: #999988; font-style: italic } +div.highlight .cp { color: #999999; font-weight: bold } +div.highlight .c1 { color: #999988; font-style: italic } +div.highlight .cs { color: #999999; font-weight: bold; font-style: italic } +div.highlight .gd { color: #000000; background-color: #ffdddd } +div.highlight .ge { font-style: italic } +div.highlight .gr { color: #aa0000 } +div.highlight .gh { color: #999999 } +div.highlight .gi { color: #000000; background-color: #ddffdd } +div.highlight .go { color: #888888 } +div.highlight .gp { color: #555555 } +div.highlight .gs { font-weight: bold } +div.highlight .gu { color: #aaaaaa } +div.highlight .gt { color: #aa0000 } +div.highlight .kc { font-weight: bold } +div.highlight .kd { font-weight: bold } +div.highlight .kp { font-weight: bold } +div.highlight .kr { font-weight: bold } +div.highlight .kt { color: #445588; font-weight: bold } +div.highlight .m { color: #009999 } +div.highlight .s { color: #bb8844 } +div.highlight .na { color: #008080 } +div.highlight .nb { color: #999999 } +div.highlight .nc { color: #445588; font-weight: bold } +div.highlight .no { color: #008080 } +div.highlight .ni { color: #800080 } +div.highlight .ne { color: #990000; font-weight: bold } +div.highlight .nf { color: #990000; font-weight: bold } +div.highlight .nn { color: #555555 } +div.highlight .nt { color: #000080 } +div.highlight .nv { color: #008080 } +div.highlight .ow { font-weight: bold } +div.highlight .mf { color: #009999 } +div.highlight .mh { color: #009999 } +div.highlight .mi { color: #009999 } +div.highlight .mo { color: #009999 } +div.highlight .sb { color: #bb8844 } +div.highlight .sc { color: #bb8844 } +div.highlight .sd { color: #bb8844 } +div.highlight .s2 { color: #bb8844 } +div.highlight .se { color: #bb8844 } +div.highlight .sh { color: #bb8844 } +div.highlight .si { color: #bb8844 } +div.highlight .sx { color: #bb8844 } +div.highlight .sr { color: #808000 } +div.highlight .s1 { color: #bb8844 } +div.highlight .ss { color: #bb8844 } +div.highlight .bp { color: #999999 } +div.highlight .vc { color: #008080 } +div.highlight .vg { color: #008080 } +div.highlight .vi { color: #008080 } +div.highlight .il { color: #009999 }
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..326cd1b37dcd637d2e8b7193fe0011f51cca3ce9 GIT binary patch literal 227 zc${<hbhEHbT)@D^u!sQ!Lc_zoeZ4HLt&EM0|1mOw#TXQSvapIUurla?I3TqQ%quud zlrDZSmDDaVyF7E}b@6Ytvoj_shOWv?&*N&2(|LPsf$hGxedl*76r8xsnPhgyIr50X zd<6}!Q;RpPnW`>y`k~e02bnvp_D$RTJdd?9OQ`;xMNQr6-*0k%)gGDlIK1ArveB)f zy1Birqq{afsXt{x+N6vrS=0I^&YBz=ojG&P!l|<tFPgq&`LYE|*W`4qZ`#nham(gy aPIaAIoocJPH|^MWAiw3{(IdQ^4AuZ)%VDtq
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..42ae3f86d7f1513fd585ad02959afbb37ad476cb GIT binary patch literal 270 zc%17D@N?(olHy`uVBq!ia0vp^0zj<5!2~3yyw0Buq&N#aB8wRqxP?KOkzv*x37{Z* ziKnkC`xPz@COHia{`NUQp|zeajv*GOr}lgD9aa!v`Tf8C>%IowZOI~+1jT1O3(*SY z=AC!{^{#V=egxhBzNNG+Wcfh@o-JZ+A^YdBKeo7Z&60On=AvAqCA)Sc&P(TZUiF#( z^rj1DlCO7NijoNWTO^lr+bufXqg3k6Z@=t^|9Q_@g4NuszNO~8s$076@rDAqInO=8 zN?HUmba__RS@I|KY$^3?e!VMWHqT1EydCdvADZy$5&PrG?DFy^Z!PAn-4+0JID@CF KpUXO@geCwp5oi<u
new file mode 100644 --- /dev/null +++ b/0.8.x/doc/support.txt @@ -0,0 +1,48 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +============================= +Support Classes and Functions +============================= + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +The ``babel.support`` modules contains a number of classes and functions that +can help with integrating Babel, and internationalization in general, into your +application or framework. The code in this module is not used by Babel itself, +but instead is provided to address common requirements of applications that +should handle internationalization. + + +--------------- +Lazy Evaluation +--------------- + +One such requirement is lazy evaluation of translations. Many web-based +applications define some localizable message at the module level, or in general +at some level where the locale of the remote user is not yet known. For such +cases, web frameworks generally provide a "lazy" variant of the ``gettext`` +functions, which basically translates the message not when the ``gettext`` +function is invoked, but when the string is accessed in some manner. + + +--------------------------- +Extended Translations Class +--------------------------- + +Many web-based applications are composed of a variety of different components +(possibly using some kind of plugin system), and some of those components may +provide their own message catalogs that need to be integrated into the larger +system. + +To support this usage pattern, Babel provides a ``Translations`` class that is +derived from the ``GNUTranslations`` class in the ``gettext`` module. This +class adds a ``merge()`` method that takes another ``Translations`` instance, +and merges the content of the latter into the main catalog: + +.. code-block:: python + + translations = Translations.load('main') + translations.merge(Translations.load('plugin1'))
new file mode 100755 --- /dev/null +++ b/0.8.x/scripts/dump_data.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +from pprint import pprint +import sys + +from babel.localedata import load + +pprint(load(sys.argv[1]))
new file mode 100755 --- /dev/null +++ b/0.8.x/scripts/import_cldr.py @@ -0,0 +1,315 @@ +#!/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 + +# Make sure we're using Babel source, and not some previously installed version +sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..')) + +from babel import dates, numbers + +weekdays = {'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3, 'fri': 4, 'sat': 5, + 'sun': 6} + +try: + any +except NameError: + def any(iterable): + return filter(None, list(iterable)) + +def _text(elem): + buf = [elem.text or ''] + for child in elem: + buf.append(_text(child)) + buf.append(elem.tail or '') + return u''.join(filter(None, buf)).strip() + +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') + + sup = parse(os.path.join(srcdir, 'supplemental', 'supplementalData.xml')) + + # build a territory containment mapping for inheritance + regions = {} + for elem in sup.findall('//territoryContainment/group'): + regions[elem.attrib['type']] = elem.attrib['contains'].split() + + # Resolve territory containment + territory_containment = {} + region_items = regions.items() + region_items.sort() + for group, territory_list in region_items: + for territory in territory_list: + containers = territory_containment.setdefault(territory, set([])) + if group in territory_containment: + containers |= territory_containment[group] + containers.add(group) + + 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 + + tree = parse(os.path.join(srcdir, 'main', filename)) + data = {} + + language = None + elem = tree.find('//identity/language') + if elem is not None: + language = elem.attrib['type'] + print>>sys.stderr, ' Language: %r' % language + + territory = None + elem = tree.find('//identity/territory') + if elem is not None: + territory = elem.attrib['type'] + else: + territory = '001' # world + print>>sys.stderr, ' Territory: %r' % territory + regions = territory_containment.get(territory, []) + print>>sys.stderr, ' Regions: %r' % regions + + # <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> + + week_data = data.setdefault('week_data', {}) + supelem = sup.find('//weekData') + + for elem in supelem.findall('minDays'): + territories = elem.attrib['territories'].split() + if territory in territories or any([r in territories for r in regions]): + week_data['min_days'] = int(elem.attrib['count']) + + for elem in supelem.findall('firstDay'): + territories = elem.attrib['territories'].split() + if territory in territories or any([r in territories for r in regions]): + week_data['first_day'] = weekdays[elem.attrib['day']] + + for elem in supelem.findall('weekendStart'): + territories = elem.attrib['territories'].split() + if territory in territories or any([r in territories for r in regions]): + week_data['weekend_start'] = weekdays[elem.attrib['day']] + + for elem in supelem.findall('weekendEnd'): + territories = elem.attrib['territories'].split() + if territory in territories or any([r in territories for r in regions]): + week_data['weekend_end'] = weekdays[elem.attrib['day']] + + time_zones = data.setdefault('time_zones', {}) + for elem in tree.findall('//timeZoneNames/zone'): + info = {} + city = elem.findtext('exemplarCity') + if city: + info['city'] = unicode(city) + for child in elem.findall('long/*'): + info.setdefault('long', {})[child.tag] = unicode(child.text) + for child in elem.findall('short/*'): + info.setdefault('short', {})[child.tag] = unicode(child.text) + time_zones[elem.attrib['type']] = info + + zone_aliases = data.setdefault('zone_aliases', {}) + if stem == 'root': + for elem in sup.findall('//timezoneData/zoneFormatting/zoneItem'): + if 'aliases' in elem.attrib: + canonical_id = elem.attrib['type'] + for alias in elem.attrib['aliases'].split(): + zone_aliases[alias] = canonical_id + + 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 = weekdays[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')] = \ + dates.parse_pattern(unicode(elem.findtext('dateFormat/pattern'))) + except ValueError, e: + print>>sys.stderr, 'ERROR: %s' % 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')] = \ + dates.parse_pattern(unicode(elem.findtext('timeFormat/pattern'))) + except ValueError, e: + print>>sys.stderr, 'ERROR: %s' % e + + 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 + + # <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 + 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: + 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: + 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: + continue + pattern = unicode(elem.findtext('percentFormat/pattern')) + percent_formats[elem.attrib.get('type')] = numbers.parse_pattern(pattern) + + currency_names = data.setdefault('currency_names', {}) + currency_symbols = data.setdefault('currency_symbols', {}) + for elem in tree.findall('//currencies/currency'): + name = elem.findtext('displayName') + if name: + currency_names[elem.attrib['type']] = unicode(name) + symbol = elem.findtext('symbol') + if symbol: + currency_symbols[elem.attrib['type']] = unicode(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()
new file mode 100644 --- /dev/null +++ b/0.8.x/setup.cfg @@ -0,0 +1,3 @@ +[egg_info] +tag_build = dev +tag_svn_revision = true
new file mode 100755 --- /dev/null +++ b/0.8.x/setup.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +from distutils.cmd import Command +import doctest +from glob import glob +import os +try: + from setuptools import setup +except ImportError: + from distutils.core import setup +import sys + + +class build_doc(Command): + description = 'Builds the documentation' + user_options = [ + ('force', None, + "force regeneration even if no reStructuredText files have changed"), + ('without-apidocs', None, + "whether to skip the generation of API documentaton"), + ] + boolean_options = ['force', 'without-apidocs'] + + def initialize_options(self): + self.force = False + self.without_apidocs = False + + def finalize_options(self): + pass + + def run(self): + from docutils.core import publish_cmdline + from docutils.nodes import raw + from docutils.parsers import rst + + docutils_conf = os.path.join('doc', 'conf', 'docutils.ini') + epydoc_conf = os.path.join('doc', 'conf', 'epydoc.ini') + + try: + from pygments import highlight + from pygments.lexers import get_lexer_by_name + from pygments.formatters import HtmlFormatter + + def code_block(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + lexer = get_lexer_by_name(arguments[0]) + html = highlight('\n'.join(content), lexer, HtmlFormatter()) + return [raw('', html, format='html')] + code_block.arguments = (1, 0, 0) + code_block.options = {'language' : rst.directives.unchanged} + code_block.content = 1 + rst.directives.register_directive('code-block', code_block) + except ImportError: + print 'Pygments not installed, syntax highlighting disabled' + + for source in glob('doc/*.txt'): + dest = os.path.splitext(source)[0] + '.html' + if self.force or 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]) + + if not self.without_apidocs: + 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.8', + description = 'Internationalization utilities', + long_description = \ +"""A collection of tools for internationalizing Python applications.""", + author = 'Edgewall Software', + author_email = 'info@edgewall.org', + license = 'BSD', + url = 'http://babel.edgewall.org/', + download_url = 'http://babel.edgewall.org/wiki/Download', + zip_safe = False, + + classifiers = [ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + packages = ['babel', 'babel.messages'], + package_data = {'babel': ['localedata/*.dat']}, + test_suite = 'babel.tests.suite', + + entry_points = """ + [console_scripts] + babel = babel.messages.frontend:main + + [distutils.commands] + extract_messages = babel.messages.frontend:extract_messages + new_catalog = babel.messages.frontend:new_catalog + + [distutils.setup_keywords] + message_extractors = babel.messages.frontend:check_message_extractors + + [babel.extractors] + ignore = babel.messages.extract:extract_nothing + python = babel.messages.extract:extract_python + """, + + cmdclass = {'build_doc': build_doc, 'test_doc': test_doc} +)