[3] | 1 | # -*- coding: utf-8 -*- |
---|
| 2 | # |
---|
| 3 | # Copyright (C) 2007 Edgewall Software |
---|
| 4 | # All rights reserved. |
---|
| 5 | # |
---|
| 6 | # This software is licensed as described in the file COPYING, which |
---|
| 7 | # you should have received as part of this distribution. The terms |
---|
| 8 | # are also available at http://babel.edgewall.org/wiki/License. |
---|
| 9 | # |
---|
| 10 | # This software consists of voluntary contributions made by many |
---|
| 11 | # individuals. For the exact contribution history, see the revision |
---|
| 12 | # history and logs, available at http://babel.edgewall.org/log/. |
---|
| 13 | |
---|
| 14 | """Several classes and functions that help with integrating and using Babel |
---|
| 15 | in applications. |
---|
| 16 | |
---|
| 17 | .. note: the code in this module is not used by Babel itself |
---|
| 18 | """ |
---|
| 19 | |
---|
| 20 | from datetime import date, datetime, time |
---|
| 21 | import gettext |
---|
| 22 | |
---|
| 23 | try: |
---|
| 24 | set |
---|
| 25 | except NameError: |
---|
| 26 | from sets import set |
---|
| 27 | |
---|
| 28 | from babel.core import Locale |
---|
| 29 | from babel.dates import format_date, format_datetime, format_time, LC_TIME |
---|
| 30 | from babel.numbers import format_number, format_decimal, format_currency, \ |
---|
| 31 | format_percent, format_scientific, LC_NUMERIC |
---|
| 32 | from babel.util import UTC |
---|
| 33 | |
---|
| 34 | __all__ = ['Format', 'LazyProxy', 'Translations'] |
---|
| 35 | __docformat__ = 'restructuredtext en' |
---|
| 36 | |
---|
| 37 | |
---|
| 38 | class Format(object): |
---|
| 39 | """Wrapper class providing the various date and number formatting functions |
---|
| 40 | bound to a specific locale and time-zone. |
---|
| 41 | |
---|
| 42 | >>> fmt = Format('en_US', UTC) |
---|
| 43 | >>> fmt.date(date(2007, 4, 1)) |
---|
| 44 | u'Apr 1, 2007' |
---|
| 45 | >>> fmt.decimal(1.2345) |
---|
| 46 | u'1.234' |
---|
| 47 | """ |
---|
| 48 | |
---|
| 49 | def __init__(self, locale, tzinfo=None): |
---|
| 50 | """Initialize the formatter. |
---|
| 51 | |
---|
| 52 | :param locale: the locale identifier or `Locale` instance |
---|
| 53 | :param tzinfo: the time-zone info (a `tzinfo` instance or `None`) |
---|
| 54 | """ |
---|
| 55 | self.locale = Locale.parse(locale) |
---|
| 56 | self.tzinfo = tzinfo |
---|
| 57 | |
---|
| 58 | def date(self, date=None, format='medium'): |
---|
| 59 | """Return a date formatted according to the given pattern. |
---|
| 60 | |
---|
| 61 | >>> fmt = Format('en_US') |
---|
| 62 | >>> fmt.date(date(2007, 4, 1)) |
---|
| 63 | u'Apr 1, 2007' |
---|
| 64 | |
---|
| 65 | :see: `babel.dates.format_date` |
---|
| 66 | """ |
---|
| 67 | return format_date(date, format, locale=self.locale) |
---|
| 68 | |
---|
| 69 | def datetime(self, datetime=None, format='medium'): |
---|
| 70 | """Return a date and time formatted according to the given pattern. |
---|
| 71 | |
---|
| 72 | >>> from pytz import timezone |
---|
| 73 | >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern')) |
---|
| 74 | >>> fmt.datetime(datetime(2007, 4, 1, 15, 30)) |
---|
| 75 | u'Apr 1, 2007 11:30:00 AM' |
---|
| 76 | |
---|
| 77 | :see: `babel.dates.format_datetime` |
---|
| 78 | """ |
---|
| 79 | return format_datetime(datetime, format, tzinfo=self.tzinfo, |
---|
| 80 | locale=self.locale) |
---|
| 81 | |
---|
| 82 | def time(self, time=None, format='medium'): |
---|
| 83 | """Return a time formatted according to the given pattern. |
---|
| 84 | |
---|
| 85 | >>> from pytz import timezone |
---|
| 86 | >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern')) |
---|
| 87 | >>> fmt.time(datetime(2007, 4, 1, 15, 30)) |
---|
| 88 | u'11:30:00 AM' |
---|
| 89 | |
---|
| 90 | :see: `babel.dates.format_time` |
---|
| 91 | """ |
---|
| 92 | return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale) |
---|
| 93 | |
---|
| 94 | def number(self, number): |
---|
| 95 | """Return an integer number formatted for the locale. |
---|
| 96 | |
---|
| 97 | >>> fmt = Format('en_US') |
---|
| 98 | >>> fmt.number(1099) |
---|
| 99 | u'1,099' |
---|
| 100 | |
---|
| 101 | :see: `babel.numbers.format_number` |
---|
| 102 | """ |
---|
| 103 | return format_number(number, locale=self.locale) |
---|
| 104 | |
---|
| 105 | def decimal(self, number, format=None): |
---|
| 106 | """Return a decimal number formatted for the locale. |
---|
| 107 | |
---|
| 108 | >>> fmt = Format('en_US') |
---|
| 109 | >>> fmt.decimal(1.2345) |
---|
| 110 | u'1.234' |
---|
| 111 | |
---|
| 112 | :see: `babel.numbers.format_decimal` |
---|
| 113 | """ |
---|
| 114 | return format_decimal(number, format, locale=self.locale) |
---|
| 115 | |
---|
| 116 | def currency(self, number, currency): |
---|
| 117 | """Return a number in the given currency formatted for the locale. |
---|
| 118 | |
---|
| 119 | :see: `babel.numbers.format_currency` |
---|
| 120 | """ |
---|
| 121 | return format_currency(number, currency, locale=self.locale) |
---|
| 122 | |
---|
| 123 | def percent(self, number, format=None): |
---|
| 124 | """Return a number formatted as percentage for the locale. |
---|
| 125 | |
---|
| 126 | >>> fmt = Format('en_US') |
---|
| 127 | >>> fmt.percent(0.34) |
---|
| 128 | u'34%' |
---|
| 129 | |
---|
| 130 | :see: `babel.numbers.format_percent` |
---|
| 131 | """ |
---|
| 132 | return format_percent(number, format, locale=self.locale) |
---|
| 133 | |
---|
| 134 | def scientific(self, number): |
---|
| 135 | """Return a number formatted using scientific notation for the locale. |
---|
| 136 | |
---|
| 137 | :see: `babel.numbers.format_scientific` |
---|
| 138 | """ |
---|
| 139 | return format_scientific(number, locale=self.locale) |
---|
| 140 | |
---|
| 141 | |
---|
| 142 | class LazyProxy(object): |
---|
| 143 | """Class for proxy objects that delegate to a specified function to evaluate |
---|
| 144 | the actual object. |
---|
| 145 | |
---|
| 146 | >>> def greeting(name='world'): |
---|
| 147 | ... return 'Hello, %s!' % name |
---|
| 148 | >>> lazy_greeting = LazyProxy(greeting, name='Joe') |
---|
| 149 | >>> print lazy_greeting |
---|
| 150 | Hello, Joe! |
---|
| 151 | >>> u' ' + lazy_greeting |
---|
| 152 | u' Hello, Joe!' |
---|
| 153 | >>> u'(%s)' % lazy_greeting |
---|
| 154 | u'(Hello, Joe!)' |
---|
| 155 | |
---|
| 156 | This can be used, for example, to implement lazy translation functions that |
---|
| 157 | delay the actual translation until the string is actually used. The |
---|
| 158 | rationale for such behavior is that the locale of the user may not always |
---|
| 159 | be available. In web applications, you only know the locale when processing |
---|
| 160 | a request. |
---|
| 161 | |
---|
| 162 | The proxy implementation attempts to be as complete as possible, so that |
---|
| 163 | the lazy objects should mostly work as expected, for example for sorting: |
---|
| 164 | |
---|
| 165 | >>> greetings = [ |
---|
| 166 | ... LazyProxy(greeting, 'world'), |
---|
| 167 | ... LazyProxy(greeting, 'Joe'), |
---|
| 168 | ... LazyProxy(greeting, 'universe'), |
---|
| 169 | ... ] |
---|
| 170 | >>> greetings.sort() |
---|
| 171 | >>> for greeting in greetings: |
---|
| 172 | ... print greeting |
---|
| 173 | Hello, Joe! |
---|
| 174 | Hello, universe! |
---|
| 175 | Hello, world! |
---|
| 176 | """ |
---|
| 177 | __slots__ = ['_func', '_args', '_kwargs', '_value'] |
---|
| 178 | |
---|
| 179 | def __init__(self, func, *args, **kwargs): |
---|
| 180 | # Avoid triggering our own __setattr__ implementation |
---|
| 181 | object.__setattr__(self, '_func', func) |
---|
| 182 | object.__setattr__(self, '_args', args) |
---|
| 183 | object.__setattr__(self, '_kwargs', kwargs) |
---|
| 184 | object.__setattr__(self, '_value', None) |
---|
| 185 | |
---|
| 186 | def value(self): |
---|
| 187 | if self._value is None: |
---|
| 188 | value = self._func(*self._args, **self._kwargs) |
---|
| 189 | object.__setattr__(self, '_value', value) |
---|
| 190 | return self._value |
---|
| 191 | value = property(value) |
---|
| 192 | |
---|
| 193 | def __contains__(self, key): |
---|
| 194 | return key in self.value |
---|
| 195 | |
---|
| 196 | def __nonzero__(self): |
---|
| 197 | return bool(self.value) |
---|
| 198 | |
---|
| 199 | def __dir__(self): |
---|
| 200 | return dir(self.value) |
---|
| 201 | |
---|
| 202 | def __iter__(self): |
---|
| 203 | return iter(self.value) |
---|
| 204 | |
---|
| 205 | def __len__(self): |
---|
| 206 | return len(self.value) |
---|
| 207 | |
---|
| 208 | def __str__(self): |
---|
| 209 | return str(self.value) |
---|
| 210 | |
---|
| 211 | def __unicode__(self): |
---|
| 212 | return unicode(self.value) |
---|
| 213 | |
---|
| 214 | def __add__(self, other): |
---|
| 215 | return self.value + other |
---|
| 216 | |
---|
| 217 | def __radd__(self, other): |
---|
| 218 | return other + self.value |
---|
| 219 | |
---|
| 220 | def __mod__(self, other): |
---|
| 221 | return self.value % other |
---|
| 222 | |
---|
| 223 | def __rmod__(self, other): |
---|
| 224 | return other % self.value |
---|
| 225 | |
---|
| 226 | def __mul__(self, other): |
---|
| 227 | return self.value * other |
---|
| 228 | |
---|
| 229 | def __rmul__(self, other): |
---|
| 230 | return other * self.value |
---|
| 231 | |
---|
| 232 | def __call__(self, *args, **kwargs): |
---|
| 233 | return self.value(*args, **kwargs) |
---|
| 234 | |
---|
| 235 | def __lt__(self, other): |
---|
| 236 | return self.value < other |
---|
| 237 | |
---|
| 238 | def __le__(self, other): |
---|
| 239 | return self.value <= other |
---|
| 240 | |
---|
| 241 | def __eq__(self, other): |
---|
| 242 | return self.value == other |
---|
| 243 | |
---|
| 244 | def __ne__(self, other): |
---|
| 245 | return self.value != other |
---|
| 246 | |
---|
| 247 | def __gt__(self, other): |
---|
| 248 | return self.value > other |
---|
| 249 | |
---|
| 250 | def __ge__(self, other): |
---|
| 251 | return self.value >= other |
---|
| 252 | |
---|
| 253 | def __delattr__(self, name): |
---|
| 254 | delattr(self.value, name) |
---|
| 255 | |
---|
| 256 | def __getattr__(self, name): |
---|
| 257 | return getattr(self.value, name) |
---|
| 258 | |
---|
| 259 | def __setattr__(self, name, value): |
---|
| 260 | setattr(self.value, name, value) |
---|
| 261 | |
---|
| 262 | def __delitem__(self, key): |
---|
| 263 | del self.value[key] |
---|
| 264 | |
---|
| 265 | def __getitem__(self, key): |
---|
| 266 | return self.value[key] |
---|
| 267 | |
---|
| 268 | def __setitem__(self, key, value): |
---|
| 269 | self.value[key] = value |
---|
| 270 | |
---|
| 271 | |
---|
| 272 | class Translations(gettext.GNUTranslations, object): |
---|
| 273 | """An extended translation catalog class.""" |
---|
| 274 | |
---|
| 275 | DEFAULT_DOMAIN = 'messages' |
---|
| 276 | |
---|
| 277 | def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN): |
---|
| 278 | """Initialize the translations catalog. |
---|
| 279 | |
---|
| 280 | :param fileobj: the file-like object the translation should be read |
---|
| 281 | from |
---|
| 282 | """ |
---|
| 283 | gettext.GNUTranslations.__init__(self, fp=fileobj) |
---|
| 284 | self.files = filter(None, [getattr(fileobj, 'name', None)]) |
---|
| 285 | self.domain = domain |
---|
| 286 | self._domains = {} |
---|
| 287 | |
---|
| 288 | def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN): |
---|
| 289 | """Load translations from the given directory. |
---|
| 290 | |
---|
| 291 | :param dirname: the directory containing the ``MO`` files |
---|
| 292 | :param locales: the list of locales in order of preference (items in |
---|
| 293 | this list can be either `Locale` objects or locale |
---|
| 294 | strings) |
---|
| 295 | :param domain: the message domain |
---|
| 296 | :return: the loaded catalog, or a ``NullTranslations`` instance if no |
---|
| 297 | matching translations were found |
---|
| 298 | :rtype: `Translations` |
---|
| 299 | """ |
---|
| 300 | if locales is not None: |
---|
| 301 | if not isinstance(locales, (list, tuple)): |
---|
| 302 | locales = [locales] |
---|
| 303 | locales = [str(locale) for locale in locales] |
---|
| 304 | if not domain: |
---|
| 305 | domain = cls.DEFAULT_DOMAIN |
---|
| 306 | filename = gettext.find(domain, dirname, locales) |
---|
| 307 | if not filename: |
---|
| 308 | return gettext.NullTranslations() |
---|
| 309 | return cls(fileobj=open(filename, 'rb'), domain=domain) |
---|
| 310 | load = classmethod(load) |
---|
| 311 | |
---|
| 312 | def __repr__(self): |
---|
| 313 | return '<%s: "%s">' % (type(self).__name__, |
---|
| 314 | self._info.get('project-id-version')) |
---|
| 315 | |
---|
| 316 | def add(self, translations, merge=True): |
---|
| 317 | """Add the given translations to the catalog. |
---|
| 318 | |
---|
| 319 | If the domain of the translations is different than that of the |
---|
| 320 | current catalog, they are added as a catalog that is only accessible |
---|
| 321 | by the various ``d*gettext`` functions. |
---|
| 322 | |
---|
| 323 | :param translations: the `Translations` instance with the messages to |
---|
| 324 | add |
---|
| 325 | :param merge: whether translations for message domains that have |
---|
| 326 | already been added should be merged with the existing |
---|
| 327 | translations |
---|
| 328 | :return: the `Translations` instance (``self``) so that `merge` calls |
---|
| 329 | can be easily chained |
---|
| 330 | :rtype: `Translations` |
---|
| 331 | """ |
---|
| 332 | domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN) |
---|
| 333 | if merge and domain == self.domain: |
---|
| 334 | return self.merge(translations) |
---|
| 335 | |
---|
| 336 | existing = self._domains.get(domain) |
---|
| 337 | if merge and existing is not None: |
---|
| 338 | existing.merge(translations) |
---|
| 339 | else: |
---|
| 340 | translations.add_fallback(self) |
---|
| 341 | self._domains[domain] = translations |
---|
| 342 | |
---|
| 343 | return self |
---|
| 344 | |
---|
| 345 | def merge(self, translations): |
---|
| 346 | """Merge the given translations into the catalog. |
---|
| 347 | |
---|
| 348 | Message translations in the specified catalog override any messages |
---|
| 349 | with the same identifier in the existing catalog. |
---|
| 350 | |
---|
| 351 | :param translations: the `Translations` instance with the messages to |
---|
| 352 | merge |
---|
| 353 | :return: the `Translations` instance (``self``) so that `merge` calls |
---|
| 354 | can be easily chained |
---|
| 355 | :rtype: `Translations` |
---|
| 356 | """ |
---|
| 357 | if isinstance(translations, gettext.GNUTranslations): |
---|
| 358 | self._catalog.update(translations._catalog) |
---|
| 359 | if isinstance(translations, Translations): |
---|
| 360 | self.files.extend(translations.files) |
---|
| 361 | |
---|
| 362 | return self |
---|
| 363 | |
---|
| 364 | def dgettext(self, domain, message): |
---|
| 365 | """Like ``gettext()``, but look the message up in the specified |
---|
| 366 | domain. |
---|
| 367 | """ |
---|
| 368 | return self._domains.get(domain, self).gettext(message) |
---|
| 369 | |
---|
| 370 | def ldgettext(self, domain, message): |
---|
| 371 | """Like ``lgettext()``, but look the message up in the specified |
---|
| 372 | domain. |
---|
| 373 | """ |
---|
| 374 | return self._domains.get(domain, self).lgettext(message) |
---|
| 375 | |
---|
| 376 | def dugettext(self, domain, message): |
---|
| 377 | """Like ``ugettext()``, but look the message up in the specified |
---|
| 378 | domain. |
---|
| 379 | """ |
---|
| 380 | return self._domains.get(domain, self).ugettext(message) |
---|
| 381 | |
---|
| 382 | def dngettext(self, domain, singular, plural, num): |
---|
| 383 | """Like ``ngettext()``, but look the message up in the specified |
---|
| 384 | domain. |
---|
| 385 | """ |
---|
| 386 | return self._domains.get(domain, self).ngettext(singular, plural, num) |
---|
| 387 | |
---|
| 388 | def ldngettext(self, domain, singular, plural, num): |
---|
| 389 | """Like ``lngettext()``, but look the message up in the specified |
---|
| 390 | domain. |
---|
| 391 | """ |
---|
| 392 | return self._domains.get(domain, self).lngettext(singular, plural, num) |
---|
| 393 | |
---|
| 394 | def dungettext(self, domain, singular, plural, num): |
---|
| 395 | """Like ``ungettext()`` but look the message up in the specified |
---|
| 396 | domain. |
---|
| 397 | """ |
---|
| 398 | return self._domains.get(domain, self).ungettext(singular, plural, num) |
---|