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) |
---|