summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog5
-rw-r--r--babel/compat.py6
-rw-r--r--babel/core.py56
-rw-r--r--babel/dates.py158
-rwxr-xr-xbabel/messages/frontend.py77
-rw-r--r--babel/messages/tests/test_frontend.py140
-rw-r--r--babel/numbers.py37
-rw-r--r--babel/support.py12
-rw-r--r--babel/tests/test_dates.py21
-rw-r--r--babel/tests/test_support.py3
-rw-r--r--babel/util.py9
-rwxr-xr-xscripts/import_cldr.py18
-rwxr-xr-xsetup.py67
13 files changed, 412 insertions, 197 deletions
diff --git a/ChangeLog b/ChangeLog
index af87275..74b8e88 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -59,6 +59,11 @@ http://svn.edgewall.org/repos/babel/tags/1.0.0/
Étienne Bersac)
* ensure .mo file header contains the same information as the source .po file
(#199)
+ * added support for get_language_name() on the locale objects.
+ * added support for get_territory_name() on the locale objects.
+ * added support for get_script_name() on the locale objects.
+ * added pluralization support for currency names and added a '¤¤¤'
+ pattern for currencies that includes the full name.
Version 0.9.6
diff --git a/babel/compat.py b/babel/compat.py
index 8aab36f..c9d407a 100644
--- a/babel/compat.py
+++ b/babel/compat.py
@@ -12,12 +12,6 @@
# history and logs, available at http://babel.edgewall.org/log/.
try:
- any = any
-except NameError:
- def any(iterable):
- return filter(None, list(iterable))
-
-try:
import threading
except ImportError:
import dummy_threading as threading
diff --git a/babel/core.py b/babel/core.py
index 42dcf7d..a72684d 100644
--- a/babel/core.py
+++ b/babel/core.py
@@ -287,6 +287,57 @@ class Locale(object):
:type: `unicode`
""")
+ def get_language_name(self, locale=None):
+ """Return the language of this locale in the given locale.
+
+ >>> Locale('zh', 'CN', script='Hans').get_language_name('de')
+ u'Chinesisch'
+
+ .. versionadded:: 1.0
+
+ :param locale: the locale to use
+ :return: the display name of the language
+ """
+ if locale is None:
+ locale = self
+ locale = Locale.parse(locale)
+ return locale.languages.get(self.language)
+
+ language_name = property(get_language_name, doc="""\
+ The localized language name of the locale.
+
+ >>> Locale('en', 'US').language_name
+ u'English'
+ """)
+
+ def get_territory_name(self, locale=None):
+ """Return the territory name in the given locale."""
+ if locale is None:
+ locale = self
+ locale = Locale.parse(locale)
+ return locale.territories.get(self.territory)
+
+ territory_name = property(get_territory_name, doc="""\
+ The localized territory name of the locale if available.
+
+ >>> Locale('de', 'DE').territory_name
+ u'Deutschland'
+ """)
+
+ def get_script_name(self, locale=None):
+ """Return the script name in the given locale."""
+ if locale is None:
+ locale = self
+ locale = Locale.parse(locale)
+ return locale.scripts.get(self.script)
+
+ script_name = property(get_script_name, doc="""\
+ The localized script name of the locale if available.
+
+ >>> Locale('ms', 'SG', script='Latn').script_name
+ u'Latin'
+ """)
+
@property
def english_name(self):
"""The english display name of the locale.
@@ -348,7 +399,10 @@ class Locale(object):
@property
def currencies(self):
- """Mapping of currency codes to translated currency names.
+ """Mapping of currency codes to translated currency names. This
+ only returns the generic form of the currency name, not the count
+ specific one. If an actual number is requested use the
+ :func:`babel.numbers.get_currency_name` function.
>>> Locale('en').currencies['COP']
u'Colombian Peso'
diff --git a/babel/dates.py b/babel/dates.py
index 642195b..562abf1 100644
--- a/babel/dates.py
+++ b/babel/dates.py
@@ -23,10 +23,12 @@ following environment variables, in that order:
from __future__ import division
from datetime import date, datetime, time, timedelta
+from bisect import bisect_right
import re
+import pytz as _pytz
from babel.core import default_locale, get_global, Locale
-from babel.util import UTC
+from babel.util import UTC, LOCALTZ
__all__ = ['format_date', 'format_datetime', 'format_time', 'format_timedelta',
'get_timezone_name', 'parse_date', 'parse_datetime', 'parse_time']
@@ -39,6 +41,78 @@ date_ = date
datetime_ = datetime
time_ = time
+def get_timezone(zone):
+ # XXX: return _pytz.timezone with a nice fallback.
+ if zone is None:
+ return LOCALTZ
+ if not isinstance(zone, basestring):
+ return zone
+ try:
+ return _pytz.timezone(zone)
+ except _pytz.UnknownTimeZoneError:
+ raise LookupError('Unknown timezone %s' % zone)
+
+def get_next_timezone_transition(zone, dt=None):
+ zone = get_timezone(zone)
+ if dt is None:
+ dt = datetime.utcnow()
+ else:
+ dt = dt.replace(tzinfo=None)
+
+ if not hasattr(zone, '_utc_transition_times'):
+ raise TypeError('Given timezone does not have UTC transition '
+ 'times. This can happen because the operating '
+ 'system local timezone is used of a custom '
+ 'timezone object')
+
+ try:
+ idx = max(0, bisect_right(zone._utc_transition_times, dt))
+ old_trans = zone._transition_info[idx - 1]
+ new_trans = zone._transition_info[idx]
+ old_tz = zone._tzinfos[old_trans]
+ new_tz = zone._tzinfos[new_trans]
+ except (LookupError, ValueError):
+ raise RuntimeError('Could not calculate transition')
+
+ return TimezoneTransition(
+ activates=zone._utc_transition_times[idx],
+ from_tzinfo=old_tz,
+ to_tzinfo=new_tz,
+ reference_date=dt
+ )
+
+
+class TimezoneTransition(object):
+
+ def __init__(self, activates, from_tzinfo, to_tzinfo, reference_date=None):
+ self.activates = activates
+ self.from_tzinfo = from_tzinfo
+ self.to_tzinfo = to_tzinfo
+ self.reference_date = reference_date
+
+ @property
+ def from_tz(self):
+ return self.from_tzinfo._tzname
+
+ @property
+ def to_tz(self):
+ return self.to_tzinfo._tzname
+
+ @property
+ def from_offset(self):
+ return self.from_tzinfo._utcoffset.total_seconds()
+
+ @property
+ def to_offset(self):
+ return self.to_tzinfo._utcoffset.total_seconds()
+
+ def __repr__(self):
+ return '<TimezoneTransition %s -> %s (%s)>' % (
+ self.from_tz,
+ self.to_tz,
+ self.activates,
+ )
+
def get_period_names(locale=LC_TIME):
"""Return the names for day periods (AM/PM) used by the locale.
@@ -178,8 +252,7 @@ def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME):
>>> get_timezone_gmt(dt, locale='en')
u'GMT+00:00'
- >>> from pytz import timezone
- >>> tz = timezone('America/Los_Angeles')
+ >>> tz = get_timezone('America/Los_Angeles')
>>> dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz)
>>> get_timezone_gmt(dt, locale='en')
u'GMT-08:00'
@@ -223,18 +296,17 @@ def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME):
The result depends on both the local display name of the country and the
city associated with the time zone:
- >>> from pytz import timezone
- >>> tz = timezone('America/St_Johns')
+ >>> tz = get_timezone('America/St_Johns')
>>> get_timezone_location(tz, locale='de_DE')
u"Kanada (St. John's) Zeit"
- >>> tz = timezone('America/Mexico_City')
+ >>> tz = get_timezone('America/Mexico_City')
>>> get_timezone_location(tz, locale='de_DE')
u'Mexiko (Mexiko-Stadt) Zeit'
If the timezone is associated with a country that uses only a single
timezone, just the localized country name is returned:
- >>> tz = timezone('Europe/Berlin')
+ >>> tz = get_timezone('Europe/Berlin')
>>> get_timezone_name(tz, locale='de_DE')
u'Mitteleurop\\xe4ische Zeit'
@@ -304,8 +376,7 @@ def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False,
r"""Return the localized display name for the given timezone. The timezone
may be specified using a ``datetime`` or `tzinfo` object.
- >>> from pytz import timezone
- >>> dt = time(15, 30, tzinfo=timezone('America/Los_Angeles'))
+ >>> dt = time(15, 30, tzinfo=get_timezone('America/Los_Angeles'))
>>> get_timezone_name(dt, locale='en_US')
u'Pacific Standard Time'
>>> get_timezone_name(dt, width='short', locale='en_US')
@@ -316,7 +387,7 @@ def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False,
time. This can be used for example for selecting timezones, or to set the
time of events that recur across DST changes:
- >>> tz = timezone('America/Los_Angeles')
+ >>> tz = get_timezone('America/Los_Angeles')
>>> get_timezone_name(tz, locale='en_US')
u'Pacific Time'
>>> get_timezone_name(tz, 'short', locale='en_US')
@@ -326,7 +397,7 @@ def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False,
is associated with a country that uses only a single timezone, the name of
that country is returned, formatted according to the locale:
- >>> tz = timezone('Europe/Berlin')
+ >>> tz = get_timezone('Europe/Berlin')
>>> get_timezone_name(tz, locale='de_DE')
u'Mitteleurop\xe4ische Zeit'
>>> get_timezone_name(tz, locale='pt_BR')
@@ -335,7 +406,7 @@ def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False,
On the other hand, if the country uses multiple timezones, the city is also
included in the representation:
- >>> tz = timezone('America/St_Johns')
+ >>> tz = get_timezone('America/St_Johns')
>>> get_timezone_name(tz, locale='de_DE')
u'Neufundland-Zeit'
@@ -458,12 +529,11 @@ def format_datetime(datetime=None, format='medium', tzinfo=None,
For any pattern requiring the display of the time-zone, the third-party
``pytz`` package is needed to explicitly specify the time-zone:
- >>> from pytz import timezone
- >>> format_datetime(dt, 'full', tzinfo=timezone('Europe/Paris'),
+ >>> format_datetime(dt, 'full', tzinfo=get_timezone('Europe/Paris'),
... locale='fr_FR')
u'dimanche 1 avril 2007 17:30:00 heure avanc\xe9e d\u2019Europe centrale'
>>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz",
- ... tzinfo=timezone('US/Eastern'), locale='en')
+ ... tzinfo=get_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
@@ -512,16 +582,15 @@ def format_time(time=None, format='medium', tzinfo=None, locale=LC_TIME):
>>> 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:
+ For any pattern requiring the display of the time-zone a
+ timezone has to be specified explicitly:
- >>> from pytz import timezone
>>> t = datetime(2007, 4, 1, 15, 30)
- >>> tzinfo = timezone('Europe/Paris')
+ >>> tzinfo = get_timezone('Europe/Paris')
>>> t = tzinfo.localize(t)
>>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR')
u'15:30:00 heure avanc\xe9e d\u2019Europe centrale'
- >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=timezone('US/Eastern'),
+ >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=get_timezone('US/Eastern'),
... locale='en')
u"09 o'clock AM, Eastern Daylight Time"
@@ -539,10 +608,10 @@ def format_time(time=None, format='medium', tzinfo=None, locale=LC_TIME):
parameter is only used to display the timezone name if needed:
>>> t = time(15, 30)
- >>> format_time(t, format='full', tzinfo=timezone('Europe/Paris'),
+ >>> format_time(t, format='full', tzinfo=get_timezone('Europe/Paris'),
... locale='fr_FR')
u'15:30:00 heure normale de l\u2019Europe centrale'
- >>> format_time(t, format='full', tzinfo=timezone('US/Eastern'),
+ >>> format_time(t, format='full', tzinfo=get_timezone('US/Eastern'),
... locale='en_US')
u'3:30:00 PM Eastern Standard Time'
@@ -589,13 +658,15 @@ TIMEDELTA_UNITS = (
('second', 1)
)
-def format_timedelta(delta, granularity='second', threshold=.85, locale=LC_TIME):
+def format_timedelta(delta, granularity='second', threshold=.85,
+ add_direction=False, format='medium',
+ locale=LC_TIME):
"""Return a time delta according to the rules of the given locale.
>>> format_timedelta(timedelta(weeks=12), locale='en_US')
- u'3 mths'
+ u'3 months'
>>> format_timedelta(timedelta(seconds=1), locale='es')
- u'1 s'
+ u'1 segundo'
The granularity parameter can be provided to alter the lowest unit
presented, which defaults to a second.
@@ -611,7 +682,15 @@ def format_timedelta(delta, granularity='second', threshold=.85, locale=LC_TIME)
>>> format_timedelta(timedelta(hours=23), threshold=0.9, locale='en_US')
u'1 day'
>>> format_timedelta(timedelta(hours=23), threshold=1.1, locale='en_US')
- u'23 hrs'
+ u'23 hours'
+
+ In addition directional information can be provided that informs
+ the user if the date is in the past or in the future:
+
+ >>> format_timedelta(timedelta(hours=1), add_direction=True)
+ u'In 1 hour'
+ >>> format_timedelta(timedelta(hours=-1), add_direction=True)
+ u'1 hour ago'
:param delta: a ``timedelta`` object representing the time difference to
format, or the delta in seconds as an `int` value
@@ -620,15 +699,32 @@ def format_timedelta(delta, granularity='second', threshold=.85, locale=LC_TIME)
"hour", "minute" or "second"
:param threshold: factor that determines at which point the presentation
switches to the next higher unit
+ :param add_direction: if this flag is set to `True` the return value will
+ include directional information. For instance a
+ positive timedelta will include the information about
+ it being in the future, a negative will be information
+ about the value being in the past.
+ :param format: the format (currently only "medium" and "short" are supported)
:param locale: a `Locale` object or a locale identifier
:rtype: `unicode`
"""
+ if format not in ('short', 'medium'):
+ raise TypeError('Format can only be one of "short" or "medium"')
if isinstance(delta, timedelta):
seconds = int((delta.days * 86400) + delta.seconds)
else:
seconds = delta
locale = Locale.parse(locale)
+ def _iter_choices(unit):
+ if add_direction:
+ if seconds >= 0:
+ yield unit + '-future'
+ else:
+ yield unit + '-past'
+ yield unit + ':' + format
+ yield unit
+
for unit, secs_per_unit in TIMEDELTA_UNITS:
value = abs(seconds) / secs_per_unit
if value >= threshold or unit == granularity:
@@ -636,7 +732,15 @@ def format_timedelta(delta, granularity='second', threshold=.85, locale=LC_TIME)
value = max(1, value)
value = int(round(value))
plural_form = locale.plural_form(value)
- pattern = locale._data['unit_patterns'][unit][plural_form]
+ pattern = None
+ for choice in _iter_choices(unit):
+ patterns = locale._data['unit_patterns'].get(choice)
+ if patterns is not None:
+ pattern = patterns[plural_form]
+ break
+ # This really should not happen
+ if pattern is None:
+ return u''
return pattern.replace('{0}', str(value))
return u''
diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py
index 90d01a4..753151e 100755
--- a/babel/messages/frontend.py
+++ b/babel/messages/frontend.py
@@ -881,11 +881,6 @@ class CommandLineInterface(object):
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:
@@ -914,47 +909,53 @@ class CommandLineInterface(object):
parser.error("'--sort-output' and '--sort-by-file' are mutually "
"exclusive")
- try:
- catalog = Catalog(project=options.project,
- version=options.version,
- msgid_bugs_address=options.msgid_bugs_address,
- copyright_holder=options.copyright_holder,
- charset=options.charset)
-
- for dirname in args:
- if not os.path.isdir(dirname):
- parser.error('%r is not a directory' % dirname)
-
- def callback(filename, method, options):
- if method == 'ignore':
- return
- filepath = os.path.normpath(os.path.join(dirname, filename))
- optstr = ''
- if options:
- optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for
- k, v in options.items()])
- self.log.info('extracting messages from %s%s', filepath,
- optstr)
+ catalog = Catalog(project=options.project,
+ version=options.version,
+ msgid_bugs_address=options.msgid_bugs_address,
+ copyright_holder=options.copyright_holder,
+ charset=options.charset)
+
+ for dirname in args:
+ if not os.path.isdir(dirname):
+ parser.error('%r is not a directory' % dirname)
+
+ def callback(filename, method, options):
+ if method == 'ignore':
+ return
+ filepath = os.path.normpath(os.path.join(dirname, filename))
+ optstr = ''
+ if options:
+ optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for
+ k, v in options.items()])
+ self.log.info('extracting messages from %s%s', filepath,
+ optstr)
+
+ extracted = extract_from_dir(dirname, method_map, options_map,
+ keywords, options.comment_tags,
+ callback=callback,
+ strip_comment_tags=
+ options.strip_comment_tags)
+ for filename, lineno, message, comments, context in extracted:
+ filepath = os.path.normpath(os.path.join(dirname, filename))
+ catalog.add(message, None, [(filepath, lineno)],
+ auto_comments=comments, context=context)
- extracted = extract_from_dir(dirname, method_map, options_map,
- keywords, options.comment_tags,
- callback=callback,
- strip_comment_tags=
- options.strip_comment_tags)
- for filename, lineno, message, comments, context in extracted:
- filepath = os.path.normpath(os.path.join(dirname, filename))
- catalog.add(message, None, [(filepath, lineno)],
- auto_comments=comments, context=context)
+ if options.output not in (None, '-'):
+ self.log.info('writing PO template file to %s' % options.output)
+ outfile = open(options.output, 'w')
+ close_output = True
+ else:
+ outfile = sys.stdout
+ close_output = False
- if options.output not in (None, '-'):
- self.log.info('writing PO template file to %s' % options.output)
+ try:
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:
+ if close_output:
outfile.close()
def init(self, argv):
diff --git a/babel/messages/tests/test_frontend.py b/babel/messages/tests/test_frontend.py
index 2d934e7..0d76b66 100644
--- a/babel/messages/tests/test_frontend.py
+++ b/babel/messages/tests/test_frontend.py
@@ -115,7 +115,8 @@ class ExtractMessagesTestCase(unittest.TestCase):
self.cmd.finalize_options()
self.cmd.run()
- catalog = read_po(open(self._pot_file(), 'U'))
+ with open(self._pot_file(), 'U') as f:
+ catalog = read_po(f)
msg = catalog.get('bar')
self.assertEqual(1, len(msg.locations))
self.assertTrue('file1.py' in msg.locations[0][0])
@@ -138,8 +139,7 @@ class ExtractMessagesTestCase(unittest.TestCase):
self.assert_pot_file_exists()
- self.assertEqual(
-r"""# Translations template for TestProject.
+ expected_content = r"""# Translations template for TestProject.
# Copyright (C) %(year)s FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -180,8 +180,10 @@ msgstr[1] ""
""" % {'version': VERSION,
'year': time.strftime('%Y'),
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
- tzinfo=LOCALTZ, locale='en')},
- open(self._pot_file(), 'U').read())
+ tzinfo=LOCALTZ, locale='en')}
+ with open(self._pot_file(), 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def test_extraction_with_mapping_file(self):
self.cmd.copyright_holder = 'FooBar, Inc.'
@@ -195,8 +197,7 @@ msgstr[1] ""
self.assert_pot_file_exists()
- self.assertEqual(
-r"""# Translations template for TestProject.
+ expected_content = r"""# Translations template for TestProject.
# Copyright (C) %(year)s FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -231,8 +232,10 @@ msgstr[1] ""
""" % {'version': VERSION,
'year': time.strftime('%Y'),
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
- tzinfo=LOCALTZ, locale='en')},
- open(self._pot_file(), 'U').read())
+ tzinfo=LOCALTZ, locale='en')}
+ with open(self._pot_file(), 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def test_extraction_with_mapping_dict(self):
self.dist.message_extractors = {
@@ -251,8 +254,7 @@ msgstr[1] ""
self.assert_pot_file_exists()
- self.assertEqual(
-r"""# Translations template for TestProject.
+ expected_content = r"""# Translations template for TestProject.
# Copyright (C) %(year)s FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -287,8 +289,10 @@ msgstr[1] ""
""" % {'version': VERSION,
'year': time.strftime('%Y'),
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
- tzinfo=LOCALTZ, locale='en')},
- open(self._pot_file(), 'U').read())
+ tzinfo=LOCALTZ, locale='en')}
+ with open(self._pot_file(), 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
class InitCatalogTestCase(unittest.TestCase):
@@ -343,8 +347,7 @@ class InitCatalogTestCase(unittest.TestCase):
po_file = self._po_file('en_US')
assert os.path.isfile(po_file)
- self.assertEqual(
-r"""# English (United States) translations for TestProject.
+ expected_content = r"""# English (United States) translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -378,8 +381,10 @@ msgstr[1] ""
""" % {'version': VERSION,
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
- tzinfo=LOCALTZ, locale='en')},
- open(po_file, 'U').read())
+ tzinfo=LOCALTZ, locale='en')}
+ with open(po_file, 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def test_keeps_catalog_non_fuzzy(self):
self.cmd.input_file = 'project/i18n/messages_non_fuzzy.pot'
@@ -392,8 +397,7 @@ msgstr[1] ""
po_file = self._po_file('en_US')
assert os.path.isfile(po_file)
- self.assertEqual(
-r"""# English (United States) translations for TestProject.
+ expected_content = r"""# English (United States) translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -427,8 +431,10 @@ msgstr[1] ""
""" % {'version': VERSION,
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
- tzinfo=LOCALTZ, locale='en')},
- open(po_file, 'U').read())
+ tzinfo=LOCALTZ, locale='en')}
+ with open(po_file, 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def test_correct_init_more_than_2_plurals(self):
self.cmd.input_file = 'project/i18n/messages.pot'
@@ -441,8 +447,7 @@ msgstr[1] ""
po_file = self._po_file('lv_LV')
assert os.path.isfile(po_file)
- self.assertEqual(
-r"""# Latvian (Latvia) translations for TestProject.
+ expected_content = r"""# Latvian (Latvia) translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -478,8 +483,10 @@ msgstr[2] ""
""" % {'version': VERSION,
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
- tzinfo=LOCALTZ, locale='en')},
- open(po_file, 'U').read())
+ tzinfo=LOCALTZ, locale='en')}
+ with open(po_file, 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def test_correct_init_singular_plural_forms(self):
self.cmd.input_file = 'project/i18n/messages.pot'
@@ -492,8 +499,7 @@ msgstr[2] ""
po_file = self._po_file('ja_JP')
assert os.path.isfile(po_file)
- self.assertEqual(
-r"""# Japanese (Japan) translations for TestProject.
+ expected_content = r"""# Japanese (Japan) translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -526,8 +532,10 @@ msgstr[0] ""
""" % {'version': VERSION,
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
- tzinfo=LOCALTZ, locale='ja_JP')},
- open(po_file, 'U').read())
+ tzinfo=LOCALTZ, locale='ja_JP')}
+ with open(po_file, 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def test_supports_no_wrap(self):
self.cmd.input_file = 'project/i18n/long_messages.pot'
@@ -536,9 +544,11 @@ msgstr[0] ""
long_message = '"'+ 'xxxxx '*15 + '"'
- pot_contents = open('project/i18n/messages.pot', 'U').read()
+ with open('project/i18n/messages.pot', 'U') as f:
+ pot_contents = f.read()
pot_with_very_long_line = pot_contents.replace('"bar"', long_message)
- open(self.cmd.input_file, 'wb').write(pot_with_very_long_line)
+ with open(self.cmd.input_file, 'wb') as f:
+ f.write(pot_with_very_long_line)
self.cmd.no_wrap = True
self.cmd.finalize_options()
@@ -546,7 +556,7 @@ msgstr[0] ""
po_file = self._po_file('en_US')
assert os.path.isfile(po_file)
- self.assertEqual(r"""# English (United States) translations for TestProject.
+ expected_content = r"""# English (United States) translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -581,8 +591,10 @@ msgstr[1] ""
""" % {'version': VERSION,
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
tzinfo=LOCALTZ, locale='en_US'),
- 'long_message': long_message},
- open(po_file, 'U').read())
+ 'long_message': long_message}
+ with open(po_file, 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def test_supports_width(self):
self.cmd.input_file = 'project/i18n/long_messages.pot'
@@ -591,17 +603,18 @@ msgstr[1] ""
long_message = '"'+ 'xxxxx '*15 + '"'
- pot_contents = open('project/i18n/messages.pot', 'U').read()
+ with open('project/i18n/messages.pot', 'U') as f:
+ pot_contents = f.read()
pot_with_very_long_line = pot_contents.replace('"bar"', long_message)
- open(self.cmd.input_file, 'wb').write(pot_with_very_long_line)
+ with open(self.cmd.input_file, 'wb') as f:
+ f.write(pot_with_very_long_line)
self.cmd.width = 120
self.cmd.finalize_options()
self.cmd.run()
po_file = self._po_file('en_US')
assert os.path.isfile(po_file)
- self.assertEqual(
-r"""# English (United States) translations for TestProject.
+ expected_content = r"""# English (United States) translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -636,8 +649,10 @@ msgstr[1] ""
""" % {'version': VERSION,
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
tzinfo=LOCALTZ, locale='en_US'),
- 'long_message': long_message},
- open(po_file, 'U').read())
+ 'long_message': long_message}
+ with open(po_file, 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
class CommandLineInterfaceTestCase(unittest.TestCase):
@@ -753,8 +768,7 @@ commands:
'-c', 'TRANSLATOR', '-c', 'TRANSLATORS:',
'-o', pot_file, 'project'])
self.assert_pot_file_exists()
- self.assertEqual(
-r"""# Translations template for TestProject.
+ expected_content = r"""# Translations template for TestProject.
# Copyright (C) %(year)s FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -795,8 +809,10 @@ 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())
+ tzinfo=LOCALTZ, locale='en')}
+ with open(pot_file, 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def test_extract_with_mapping_file(self):
pot_file = self._pot_file()
@@ -808,8 +824,7 @@ msgstr[1] ""
'-c', 'TRANSLATOR', '-c', 'TRANSLATORS:',
'-o', pot_file, 'project'])
self.assert_pot_file_exists()
- self.assertEqual(
-r"""# Translations template for TestProject.
+ expected_content = r"""# Translations template for TestProject.
# Copyright (C) %(year)s FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -844,8 +859,10 @@ 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())
+ tzinfo=LOCALTZ, locale='en')}
+ with open(pot_file, 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def test_init_with_output_dir(self):
po_file = self._po_file('en_US')
@@ -854,8 +871,7 @@ msgstr[1] ""
'-d', os.path.join(self._i18n_dir()),
'-i', os.path.join(self._i18n_dir(), 'messages.pot')])
assert os.path.isfile(po_file)
- self.assertEqual(
-r"""# English (United States) translations for TestProject.
+ expected_content = r"""# English (United States) translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -890,8 +906,10 @@ msgstr[1] ""
""" % {'version': VERSION,
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
- tzinfo=LOCALTZ, locale='en')},
- open(po_file, 'U').read())
+ tzinfo=LOCALTZ, locale='en')}
+ with open(po_file, 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def _i18n_dir(self):
return os.path.join(self.datadir, 'project', 'i18n')
@@ -903,8 +921,7 @@ msgstr[1] ""
'-d', os.path.join(self._i18n_dir()),
'-i', os.path.join(self._i18n_dir(), 'messages.pot')])
assert os.path.isfile(po_file)
- self.assertEqual(
-r"""# Japanese (Japan) translations for TestProject.
+ expected_content = r"""# Japanese (Japan) translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -938,8 +955,10 @@ msgstr[0] ""
""" % {'version': VERSION,
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
- tzinfo=LOCALTZ, locale='en')},
- open(po_file, 'U').read())
+ tzinfo=LOCALTZ, locale='en')}
+ with open(po_file, 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def test_init_more_than_2_plural_forms(self):
po_file = self._po_file('lv_LV')
@@ -948,8 +967,7 @@ msgstr[0] ""
'-d', self._i18n_dir(),
'-i', os.path.join(self._i18n_dir(), 'messages.pot')])
assert os.path.isfile(po_file)
- self.assertEqual(
-r"""# Latvian (Latvia) translations for TestProject.
+ expected_content = r"""# Latvian (Latvia) translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
# project.
@@ -986,8 +1004,10 @@ msgstr[2] ""
""" % {'version': VERSION,
'date': format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ',
- tzinfo=LOCALTZ, locale='en')},
- open(po_file, 'U').read())
+ tzinfo=LOCALTZ, locale='en')}
+ with open(po_file, 'U') as f:
+ actual_content = f.read()
+ self.assertEqual(expected_content, actual_content)
def test_compile_catalog(self):
po_file = self._po_file('de_DE')
diff --git a/babel/numbers.py b/babel/numbers.py
index c8980c7..cecba34 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -36,24 +36,32 @@ __all__ = ['format_number', 'format_decimal', 'format_currency',
LC_NUMERIC = default_locale('LC_NUMERIC')
-def get_currency_name(currency, locale=LC_NUMERIC):
+def get_currency_name(currency, count=None, locale=LC_NUMERIC):
"""Return the name used by the locale for the specified currency.
- >>> get_currency_name('USD', 'en_US')
+ >>> get_currency_name('USD', locale='en_US')
u'US Dollar'
:param currency: the currency code
+ :param count: the optional count. If provided the currency name
+ will be pluralized to that number if possible.
:param locale: the `Locale` object or locale identifier
:return: the currency symbol
:rtype: `unicode`
:since: version 0.9.4
"""
- return Locale.parse(locale).currencies.get(currency, currency)
+ loc = Locale.parse(locale)
+ if count is not None:
+ plural_form = loc.plural_form(count)
+ plural_names = loc._data['currency_names_plural']
+ if currency in plural_names:
+ return plural_names[currency][plural_form]
+ return loc.currencies.get(currency, currency)
def get_currency_symbol(currency, locale=LC_NUMERIC):
"""Return the symbol used by the locale for the specified currency.
- >>> get_currency_symbol('USD', 'en_US')
+ >>> get_currency_symbol('USD', locale='en_US')
u'$'
:param currency: the currency code
@@ -182,10 +190,15 @@ def format_currency(number, currency, format=None, locale=LC_NUMERIC):
>>> format_currency(1099.98, 'EUR', locale='de_DE')
u'1.099,98\\xa0\\u20ac'
- The pattern can also be specified explicitly:
+ The pattern can also be specified explicitly. The currency is
+ placed with the '¤' sign. As the sign gets repeated the format
+ expands (¤ being the symbol, ¤¤ is the currency abbreviation and
+ ¤¤¤ is the full name of the currency):
>>> format_currency(1099.98, 'EUR', u'\xa4\xa4 #,##0.00', locale='en_US')
u'EUR 1,099.98'
+ >>> format_currency(1099.98, 'EUR', u'#,##0.00 \xa4\xa4\xa4', locale='en_US')
+ u'1,099.98 euros'
:param number: the number to format
:param currency: the currency code
@@ -412,13 +425,19 @@ def parse_pattern(pattern):
if isinstance(pattern, NumberPattern):
return pattern
+ def _match_number(pattern):
+ rv = number_re.search(pattern)
+ if rv is None:
+ raise ValueError('Invalid number pattern %r' % pattern)
+ return rv.groups()
+
# 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()
+ pos_prefix, number, pos_suffix = _match_number(pattern)
+ neg_prefix, _, neg_suffix = _match_number(neg_pattern)
else:
- pos_prefix, number, pos_suffix = number_re.search(pattern).groups()
+ pos_prefix, number, pos_suffix = _match_number(pattern)
neg_prefix = '-' + pos_prefix
neg_suffix = pos_suffix
if 'E' in number:
@@ -568,6 +587,8 @@ class NumberPattern(object):
retval = u'%s%s%s' % (self.prefix[is_negative], number,
self.suffix[is_negative])
if u'¤' in retval:
+ retval = retval.replace(u'¤¤¤',
+ get_currency_name(currency, value, locale))
retval = retval.replace(u'¤¤', currency.upper())
retval = retval.replace(u'¤', get_currency_symbol(currency, locale))
return retval
diff --git a/babel/support.py b/babel/support.py
index 80f015c..fb6d3ad 100644
--- a/babel/support.py
+++ b/babel/support.py
@@ -88,17 +88,20 @@ class Format(object):
"""
return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale)
- def timedelta(self, delta, granularity='second', threshold=.85):
+ def timedelta(self, delta, granularity='second', threshold=.85,
+ format='medium', add_direction=False):
"""Return a time delta according to the rules of the given locale.
>>> fmt = Format('en_US')
>>> fmt.timedelta(timedelta(weeks=11))
- u'3 mths'
+ u'3 months'
:see: `babel.dates.format_timedelta`
"""
return format_timedelta(delta, granularity=granularity,
- threshold=threshold, locale=self.locale)
+ threshold=threshold,
+ format=format, add_direction=add_direction,
+ locale=self.locale)
def number(self, number):
"""Return an integer number formatted for the locale.
@@ -555,7 +558,8 @@ class Translations(NullTranslations, gettext.GNUTranslations):
filename = gettext.find(domain, dirname, locales)
if not filename:
return NullTranslations()
- return cls(fp=open(filename, 'rb'), domain=domain)
+ with open(filename, 'rb') as fp:
+ return cls(fp=fp, domain=domain)
def __repr__(self):
return '<%s: "%s">' % (type(self).__name__,
diff --git a/babel/tests/test_dates.py b/babel/tests/test_dates.py
index ece32aa..75e7e11 100644
--- a/babel/tests/test_dates.py
+++ b/babel/tests/test_dates.py
@@ -281,16 +281,37 @@ class FormatTimedeltaTestCase(unittest.TestCase):
def test_zero_seconds(self):
string = dates.format_timedelta(timedelta(seconds=0), locale='en')
+ self.assertEqual('0 seconds', string)
+ string = dates.format_timedelta(timedelta(seconds=0), locale='en',
+ format='short')
self.assertEqual('0 secs', string)
string = dates.format_timedelta(timedelta(seconds=0),
granularity='hour', locale='en')
+ self.assertEqual('0 hours', string)
+ string = dates.format_timedelta(timedelta(seconds=0),
+ granularity='hour', locale='en',
+ format='short')
self.assertEqual('0 hrs', string)
def test_small_value_with_granularity(self):
string = dates.format_timedelta(timedelta(seconds=42),
granularity='hour', locale='en')
+ self.assertEqual('1 hour', string)
+ string = dates.format_timedelta(timedelta(seconds=42),
+ granularity='hour', locale='en',
+ format='short')
self.assertEqual('1 hr', string)
+ def test_direction_adding(self):
+ string = dates.format_timedelta(timedelta(hours=1),
+ locale='en',
+ add_direction=True)
+ self.assertEqual('In 1 hour', string)
+ string = dates.format_timedelta(timedelta(hours=-1),
+ locale='en',
+ add_direction=True)
+ self.assertEqual('1 hour ago', string)
+
class TimeZoneAdjustTestCase(unittest.TestCase):
def _utc(self):
diff --git a/babel/tests/test_support.py b/babel/tests/test_support.py
index 00b8f18..f2e4796 100644
--- a/babel/tests/test_support.py
+++ b/babel/tests/test_support.py
@@ -173,7 +173,8 @@ class TranslationsTestCase(unittest.TestCase):
os.makedirs(messages_dir)
catalog = Catalog(locale='fr', domain='messages')
catalog.add('foo', 'bar')
- write_mo(file(os.path.join(messages_dir, 'messages.mo'), 'wb'), catalog)
+ with open(os.path.join(messages_dir, 'messages.mo'), 'wb') as f:
+ write_mo(f, catalog)
translations = support.Translations.load(tempdir, locales=('fr',), domain='messages')
self.assertEqual('bar', translations.gettext('foo'))
diff --git a/babel/util.py b/babel/util.py
index 8eb7f69..fe202d7 100644
--- a/babel/util.py
+++ b/babel/util.py
@@ -294,14 +294,9 @@ class FixedOffsetTimezone(tzinfo):
return ZERO
-try:
- from pytz import UTC
-except ImportError:
- UTC = FixedOffsetTimezone(0, 'UTC')
- """`tzinfo` object for UTC (Universal Time).
+import pytz as _pytz
- :type: `tzinfo`
- """
+UTC = _pytz.utc
STDOFFSET = timedelta(seconds = -time.timezone)
if time.daylight:
diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py
index 7cdd114..60aeace 100755
--- a/scripts/import_cldr.py
+++ b/scripts/import_cldr.py
@@ -23,7 +23,6 @@ from xml.etree import ElementTree
sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..'))
from babel import dates, numbers
-from babel.compat import any
from babel.plural import PluralRule
from babel.localedata import Alias
@@ -504,15 +503,18 @@ def main():
percent_formats[elem.attrib.get('type')] = numbers.parse_pattern(pattern)
currency_names = data.setdefault('currency_names', {})
+ currency_names_plural = data.setdefault('currency_names_plural', {})
currency_symbols = data.setdefault('currency_symbols', {})
for elem in tree.findall('.//currencies/currency'):
code = elem.attrib['type']
- # TODO: support plural rules for currency name selection
for name in elem.findall('displayName'):
- if ('draft' in name.attrib or 'count' in name.attrib) \
- and code in currency_names:
+ if ('draft' in name.attrib) and code in currency_names:
continue
- currency_names[code] = unicode(name.text)
+ if 'count' in name.attrib:
+ currency_names_plural.setdefault(code, {})[name.attrib['count']] = \
+ unicode(name.text)
+ else:
+ currency_names[code] = unicode(name.text)
# TODO: support choice patterns for currency symbol selection
symbol = elem.find('symbol')
if symbol is not None and 'draft' not in symbol.attrib \
@@ -524,9 +526,11 @@ def main():
unit_patterns = data.setdefault('unit_patterns', {})
for elem in tree.findall('.//units/unit'):
unit_type = elem.attrib['type']
- unit_pattern = unit_patterns.setdefault(unit_type, {})
for pattern in elem.findall('unitPattern'):
- unit_patterns[unit_type][pattern.attrib['count']] = \
+ box = unit_type
+ if 'alt' in pattern.attrib:
+ box += ':' + pattern.attrib['alt']
+ unit_patterns.setdefault(box, {})[pattern.attrib['count']] = \
unicode(pattern.text)
outfile = open(data_filename, 'wb')
diff --git a/setup.py b/setup.py
index 5e44a0a..8ee86e4 100755
--- a/setup.py
+++ b/setup.py
@@ -13,13 +13,8 @@
# history and logs, available at http://babel.edgewall.org/log/.
import os
-try:
- from setuptools import setup
- have_setuptools = True
-except ImportError:
- from distutils.core import setup
- have_setuptools = False
import sys
+from setuptools import setup
sys.path.append(os.path.join('doc', 'common'))
try:
@@ -28,37 +23,6 @@ except ImportError:
build_doc = test_doc = None
-extra_arguments = dict()
-if have_setuptools:
- extra_arguments = dict(
- zip_safe = False,
- test_suite = 'babel.tests.suite',
- tests_require = ['pytz'],
-
- entry_points = """
- [console_scripts]
- pybabel = babel.messages.frontend:main
-
- [distutils.commands]
- compile_catalog = babel.messages.frontend:compile_catalog
- extract_messages = babel.messages.frontend:extract_messages
- init_catalog = babel.messages.frontend:init_catalog
- update_catalog = babel.messages.frontend:update_catalog
-
- [distutils.setup_keywords]
- message_extractors = babel.messages.frontend:check_message_extractors
-
- [babel.checkers]
- num_plurals = babel.messages.checkers:num_plurals
- python_format = babel.messages.checkers:python_format
-
- [babel.extractors]
- ignore = babel.messages.extract:extract_nothing
- python = babel.messages.extract:extract_python
- javascript = babel.messages.extract:extract_javascript
- """,
- )
-
setup(
name = 'Babel',
version = '1.0',
@@ -82,8 +46,35 @@ setup(
],
packages = ['babel', 'babel.messages'],
package_data = {'babel': ['global.dat', 'localedata/*.dat']},
+ install_requires=[
+ 'pytz',
+ ],
cmdclass = {'build_doc': build_doc, 'test_doc': test_doc},
- **extra_arguments
+ zip_safe = False,
+ test_suite = 'babel.tests.suite',
+
+ entry_points = """
+ [console_scripts]
+ pybabel = babel.messages.frontend:main
+
+ [distutils.commands]
+ compile_catalog = babel.messages.frontend:compile_catalog
+ extract_messages = babel.messages.frontend:extract_messages
+ init_catalog = babel.messages.frontend:init_catalog
+ update_catalog = babel.messages.frontend:update_catalog
+
+ [distutils.setup_keywords]
+ message_extractors = babel.messages.frontend:check_message_extractors
+
+ [babel.checkers]
+ num_plurals = babel.messages.checkers:num_plurals
+ python_format = babel.messages.checkers:python_format
+
+ [babel.extractors]
+ ignore = babel.messages.extract:extract_nothing
+ python = babel.messages.extract:extract_python
+ javascript = babel.messages.extract:extract_javascript
+ """
)