1"""
2 flask_babel
3 ~~~~~~~~~~~
4
5 Implements i18n/l10n support for Flask applications based on Babel.
6
7 :copyright: (c) 2013 by Armin Ronacher, Daniel Neuhäuser.
8 :license: BSD, see LICENSE for more details.
9"""
10import os
11from dataclasses import dataclass
12from types import SimpleNamespace
13from datetime import datetime
14from contextlib import contextmanager
15from typing import List, Callable, Optional, Union
16
17from babel.support import Translations, NullTranslations
18from flask import current_app, g
19from babel import dates, numbers, support, Locale
20from pytz import timezone, UTC
21from werkzeug.datastructures import ImmutableDict
22from werkzeug.utils import cached_property
23
24from flask_babel.speaklater import LazyString
25
26
27@dataclass
28class BabelConfiguration:
29 """Application-specific configuration for Babel."""
30 default_locale: str
31 default_timezone: str
32 default_domain: str
33 default_directories: List[str]
34 translation_directories: List[str]
35
36 instance: 'Babel'
37
38 locale_selector: Optional[Callable] = None
39 timezone_selector: Optional[Callable] = None
40
41
42def get_babel(app=None) -> 'BabelConfiguration':
43 app = app or current_app
44 if not hasattr(app, 'extensions'):
45 app.extensions = {}
46 return app.extensions['babel']
47
48
49class Babel:
50 """Central controller class that can be used to configure how
51 Flask-Babel behaves. Each application that wants to use Flask-Babel
52 has to create, or run :meth:`init_app` on, an instance of this class
53 after the configuration was initialized.
54 """
55
56 default_date_formats = ImmutableDict({
57 'time': 'medium',
58 'date': 'medium',
59 'datetime': 'medium',
60 'time.short': None,
61 'time.medium': None,
62 'time.full': None,
63 'time.long': None,
64 'date.short': None,
65 'date.medium': None,
66 'date.full': None,
67 'date.long': None,
68 'datetime.short': None,
69 'datetime.medium': None,
70 'datetime.full': None,
71 'datetime.long': None,
72 })
73
74 def __init__(self, app=None, date_formats=None, configure_jinja=True, *args,
75 **kwargs):
76 """Creates a new Babel instance.
77
78 If an application is passed, it will be configured with the provided
79 arguments. Otherwise, :meth:`init_app` can be used to configure the
80 application later.
81 """
82 self._configure_jinja = configure_jinja
83 self.date_formats = date_formats
84
85 if app is not None:
86 self.init_app(app, *args, **kwargs)
87
88 def init_app(self, app, default_locale='en', default_domain='messages',
89 default_translation_directories='translations',
90 default_timezone='UTC', locale_selector=None,
91 timezone_selector=None):
92 """
93 Initializes the Babel instance for use with this specific application.
94
95 :param app: The application to configure
96 :param default_locale: The default locale to use for this application
97 :param default_domain: The default domain to use for this application
98 :param default_translation_directories: The default translation
99 directories to use for this
100 application
101 :param default_timezone: The default timezone to use for this
102 application
103 :param locale_selector: The function to use to select the locale
104 for a request
105 :param timezone_selector: The function to use to select the
106 timezone for a request
107 """
108 if not hasattr(app, 'extensions'):
109 app.extensions = {}
110
111 directories = app.config.get(
112 'BABEL_TRANSLATION_DIRECTORIES',
113 default_translation_directories
114 ).split(';')
115
116 app.extensions['babel'] = BabelConfiguration(
117 default_locale=app.config.get(
118 'BABEL_DEFAULT_LOCALE',
119 default_locale
120 ),
121 default_timezone=app.config.get(
122 'BABEL_DEFAULT_TIMEZONE',
123 default_timezone
124 ),
125 default_domain=app.config.get(
126 'BABEL_DOMAIN',
127 default_domain
128 ),
129 default_directories=directories,
130 translation_directories=list(
131 self._resolve_directories(directories, app)
132 ),
133 instance=self,
134 locale_selector=locale_selector,
135 timezone_selector=timezone_selector
136 )
137
138 # a mapping of Babel datetime format strings that can be modified
139 # to change the defaults. If you invoke :func:`format_datetime`
140 # and do not provide any format string Flask-Babel will do the
141 # following things:
142 #
143 # 1. look up ``date_formats['datetime']``. By default, ``'medium'``
144 # is returned to enforce medium length datetime formats.
145 # 2. ``date_formats['datetime.medium'] (if ``'medium'`` was
146 # returned in step one) is looked up. If the return value
147 # is anything but `None` this is used as new format string.
148 # otherwise the default for that language is used.
149 if self.date_formats is None:
150 self.date_formats = self.default_date_formats.copy()
151
152 if self._configure_jinja:
153 app.jinja_env.filters.update(
154 datetimeformat=format_datetime,
155 dateformat=format_date,
156 timeformat=format_time,
157 timedeltaformat=format_timedelta,
158 numberformat=format_number,
159 decimalformat=format_decimal,
160 currencyformat=format_currency,
161 percentformat=format_percent,
162 scientificformat=format_scientific,
163 )
164 app.jinja_env.add_extension('jinja2.ext.i18n')
165 app.jinja_env.install_gettext_callables(
166 gettext=lambda s: get_translations().ugettext(s),
167 ngettext=lambda s, p, n: get_translations().ungettext(s, p, n),
168 newstyle=True,
169 pgettext=lambda c, s: get_translations().upgettext(c, s),
170 npgettext=lambda c, s, p, n: get_translations().unpgettext(
171 c, s, p, n
172 ),
173 )
174
175 def list_translations(self):
176 """Returns a list of all the locales translations exist for. The list
177 returned will be filled with actual locale objects and not just strings.
178
179 .. note::
180
181 The default locale will always be returned, even if no translation
182 files exist for it.
183
184 .. versionadded:: 0.6
185 """
186 result = []
187
188 for dirname in get_babel().translation_directories:
189 if not os.path.isdir(dirname):
190 continue
191
192 for folder in os.listdir(dirname):
193 locale_dir = os.path.join(dirname, folder, 'LC_MESSAGES')
194 if not os.path.isdir(locale_dir):
195 continue
196
197 if any(x.endswith('.mo') for x in os.listdir(locale_dir)):
198 result.append(Locale.parse(folder))
199
200 if self.default_locale not in result:
201 result.append(self.default_locale)
202 return result
203
204 @property
205 def default_locale(self) -> Locale:
206 """The default locale from the configuration as an instance of a
207 `babel.Locale` object.
208 """
209 return Locale.parse(get_babel().default_locale)
210
211 @property
212 def default_timezone(self) -> timezone:
213 """The default timezone from the configuration as an instance of a
214 `pytz.timezone` object.
215 """
216 return timezone(get_babel().default_timezone)
217
218 @property
219 def domain(self) -> str:
220 """The message domain for the translations as a string.
221 """
222 return get_babel().default_domain
223
224 @cached_property
225 def domain_instance(self):
226 """The message domain for the translations.
227 """
228 return Domain(domain=self.domain)
229
230 @staticmethod
231 def _resolve_directories(directories: List[str], app=None):
232 for path in directories:
233 if os.path.isabs(path):
234 yield path
235 elif app is not None:
236 # We can only resolve relative paths if we have an application
237 # context.
238 yield os.path.join(app.root_path, path)
239
240
241def get_translations() -> Union[Translations, NullTranslations]:
242 """Returns the correct gettext translations that should be used for
243 this request. This will never fail and return a dummy translation
244 object if used outside the request or if a translation cannot be found.
245 """
246 return get_domain().get_translations()
247
248
249def get_locale() -> Optional[Locale]:
250 """Returns the locale that should be used for this request as
251 `babel.Locale` object. This returns `None` if used outside a request.
252 """
253 ctx = _get_current_context()
254 if ctx is None:
255 return None
256
257 locale = getattr(ctx, 'babel_locale', None)
258 if locale is None:
259 babel = get_babel()
260 if babel.locale_selector is None:
261 locale = babel.instance.default_locale
262 else:
263 rv = babel.locale_selector()
264 if rv is None:
265 locale = babel.instance.default_locale
266 else:
267 locale = Locale.parse(rv)
268 ctx.babel_locale = locale
269
270 return locale
271
272
273def get_timezone() -> Optional[timezone]:
274 """Returns the timezone that should be used for this request as
275 a `pytz.timezone` object. This returns `None` if used outside a request.
276 """
277 ctx = _get_current_context()
278 tzinfo = getattr(ctx, 'babel_tzinfo', None)
279 if tzinfo is None:
280 babel = get_babel()
281 if babel.timezone_selector is None:
282 tzinfo = babel.instance.default_timezone
283 else:
284 rv = babel.timezone_selector()
285 if rv is None:
286 tzinfo = babel.instance.default_timezone
287 else:
288 tzinfo = timezone(rv) if isinstance(rv, str) else rv
289 ctx.babel_tzinfo = tzinfo
290 return tzinfo
291
292
293def refresh():
294 """Refreshes the cached timezones and locale information. This can
295 be used to switch a translation between a request and if you want
296 the changes to take place immediately, not just with the next request::
297
298 user.timezone = request.form['timezone']
299 user.locale = request.form['locale']
300 refresh()
301 flash(gettext('Language was changed'))
302
303 Without that refresh, the :func:`~flask.flash` function would probably
304 return English text and a now German page.
305 """
306 ctx = _get_current_context()
307 for key in 'babel_locale', 'babel_tzinfo', 'babel_translations':
308 if hasattr(ctx, key):
309 delattr(ctx, key)
310
311 if hasattr(ctx, 'forced_babel_locale'):
312 ctx.babel_locale = ctx.forced_babel_locale
313
314
315@contextmanager
316def force_locale(locale):
317 """Temporarily overrides the currently selected locale.
318
319 Sometimes it is useful to switch the current locale to different one, do
320 some tasks and then revert back to the original one. For example, if the
321 user uses German on the website, but you want to email them in English,
322 you can use this function as a context manager::
323
324 with force_locale('en_US'):
325 send_email(gettext('Hello!'), ...)
326
327 :param locale: The locale to temporary switch to (ex: 'en_US').
328 """
329 ctx = _get_current_context()
330 if ctx is None:
331 yield
332 return
333
334 orig_attrs = {}
335 for key in ('babel_translations', 'babel_locale'):
336 orig_attrs[key] = getattr(ctx, key, None)
337
338 try:
339 ctx.babel_locale = Locale.parse(locale)
340 ctx.forced_babel_locale = ctx.babel_locale
341 ctx.babel_translations = None
342 yield
343 finally:
344 if hasattr(ctx, 'forced_babel_locale'):
345 del ctx.forced_babel_locale
346
347 for key, value in orig_attrs.items():
348 setattr(ctx, key, value)
349
350
351def _get_format(key, format) -> Optional[str]:
352 """A small helper for the datetime formatting functions. Looks up
353 format defaults for different kinds.
354 """
355 babel = get_babel()
356 if format is None:
357 format = babel.instance.date_formats[key]
358 if format in ('short', 'medium', 'full', 'long'):
359 rv = babel.instance.date_formats['%s.%s' % (key, format)]
360 if rv is not None:
361 format = rv
362 return format
363
364
365def to_user_timezone(datetime):
366 """Convert a datetime object to the user's timezone. This automatically
367 happens on all date formatting unless rebasing is disabled. If you need
368 to convert a :class:`datetime.datetime` object at any time to the user's
369 timezone (as returned by :func:`get_timezone`) this function can be used.
370 """
371 if datetime.tzinfo is None:
372 datetime = datetime.replace(tzinfo=UTC)
373 tzinfo = get_timezone()
374 return tzinfo.normalize(datetime.astimezone(tzinfo))
375
376
377def to_utc(datetime):
378 """Convert a datetime object to UTC and drop tzinfo. This is the
379 opposite operation to :func:`to_user_timezone`.
380 """
381 if datetime.tzinfo is None:
382 datetime = get_timezone().localize(datetime)
383 return datetime.astimezone(UTC).replace(tzinfo=None)
384
385
386def format_datetime(datetime=None, format=None, rebase=True):
387 """Return a date formatted according to the given pattern. If no
388 :class:`~datetime.datetime` object is passed, the current time is
389 assumed. By default, rebasing happens, which causes the object to
390 be converted to the user's timezone (as returned by
391 :func:`to_user_timezone`). This function formats both date and
392 time.
393
394 The format parameter can either be ``'short'``, ``'medium'``,
395 ``'long'`` or ``'full'`` (in which case the language's default for
396 that setting is used, or the default from the :attr:`Babel.date_formats`
397 mapping is used) or a format string as documented by Babel.
398
399 This function is also available in the template context as filter
400 named `datetimeformat`.
401 """
402 format = _get_format('datetime', format)
403 return _date_format(dates.format_datetime, datetime, format, rebase)
404
405
406def format_date(date=None, format=None, rebase=True):
407 """Return a date formatted according to the given pattern. If no
408 :class:`~datetime.datetime` or :class:`~datetime.date` object is passed,
409 the current time is assumed. By default, rebasing happens, which causes
410 the object to be converted to the users's timezone (as returned by
411 :func:`to_user_timezone`). This function only formats the date part
412 of a :class:`~datetime.datetime` object.
413
414 The format parameter can either be ``'short'``, ``'medium'``,
415 ``'long'`` or ``'full'`` (in which case the language's default for
416 that setting is used, or the default from the :attr:`Babel.date_formats`
417 mapping is used) or a format string as documented by Babel.
418
419 This function is also available in the template context as filter
420 named `dateformat`.
421 """
422 if rebase and isinstance(date, datetime):
423 date = to_user_timezone(date)
424 format = _get_format('date', format)
425 return _date_format(dates.format_date, date, format, rebase)
426
427
428def format_time(time=None, format=None, rebase=True):
429 """Return a time formatted according to the given pattern. If no
430 :class:`~datetime.datetime` object is passed, the current time is
431 assumed. By default, rebasing happens, which causes the object to
432 be converted to the user's timezone (as returned by
433 :func:`to_user_timezone`). This function formats both date and time.
434
435 The format parameter can either be ``'short'``, ``'medium'``,
436 ``'long'`` or ``'full'`` (in which case the language's default for
437 that setting is used, or the default from the :attr:`Babel.date_formats`
438 mapping is used) or a format string as documented by Babel.
439
440 This function is also available in the template context as filter
441 named `timeformat`.
442 """
443 format = _get_format('time', format)
444 return _date_format(dates.format_time, time, format, rebase)
445
446
447def format_timedelta(datetime_or_timedelta, granularity: str = 'second',
448 add_direction=False, threshold=0.85):
449 """Format the elapsed time from the given date to now or the given
450 timedelta.
451
452 This function is also available in the template context as filter
453 named `timedeltaformat`.
454 """
455 if isinstance(datetime_or_timedelta, datetime):
456 datetime_or_timedelta = datetime.utcnow() - datetime_or_timedelta
457 return dates.format_timedelta(
458 datetime_or_timedelta,
459 granularity,
460 threshold=threshold,
461 add_direction=add_direction,
462 locale=get_locale()
463 )
464
465
466def _date_format(formatter, obj, format, rebase, **extra):
467 """Internal helper that formats the date."""
468 locale = get_locale()
469 extra = {}
470 if formatter is not dates.format_date and rebase:
471 extra['tzinfo'] = get_timezone()
472 return formatter(obj, format, locale=locale, **extra)
473
474
475def format_number(number) -> str:
476 """Return the given number formatted for the locale in request
477
478 :param number: the number to format
479 :return: the formatted number
480 :rtype: unicode
481 """
482 locale = get_locale()
483 return numbers.format_decimal(number, locale=locale)
484
485
486def format_decimal(number, format=None) -> str:
487 """Return the given decimal number formatted for the locale in the request.
488
489 :param number: the number to format
490 :param format: the format to use
491 :return: the formatted number
492 :rtype: unicode
493 """
494 locale = get_locale()
495 return numbers.format_decimal(number, format=format, locale=locale)
496
497
498def format_currency(number, currency, format=None, currency_digits=True,
499 format_type='standard') -> str:
500 """Return the given number formatted for the locale in the request.
501
502 :param number: the number to format
503 :param currency: the currency code
504 :param format: the format to use
505 :param currency_digits: use the currency’s number of decimal digits
506 [default: True]
507 :param format_type: the currency format type to use
508 [default: standard]
509 :return: the formatted number
510 :rtype: unicode
511 """
512 locale = get_locale()
513 return numbers.format_currency(
514 number,
515 currency,
516 format=format,
517 locale=locale,
518 currency_digits=currency_digits,
519 format_type=format_type
520 )
521
522
523def format_percent(number, format=None) -> str:
524 """Return formatted percent value for the locale in the request.
525
526 :param number: the number to format
527 :param format: the format to use
528 :return: the formatted percent number
529 :rtype: unicode
530 """
531 locale = get_locale()
532 return numbers.format_percent(number, format=format, locale=locale)
533
534
535def format_scientific(number, format=None) -> str:
536 """Return value formatted in scientific notation for the locale in request
537
538 :param number: the number to format
539 :param format: the format to use
540 :return: the formatted percent number
541 :rtype: unicode
542 """
543 locale = get_locale()
544 return numbers.format_scientific(number, format=format, locale=locale)
545
546
547class Domain(object):
548 """Localization domain. By default, it will look for translations in the
549 Flask application directory and "messages" domain - all message catalogs
550 should be called ``messages.mo``.
551
552 Additional domains are supported passing a list of domain names to the
553 ``domain`` argument, but note that in this case they must match a list
554 passed to ``translation_directories``, eg::
555
556 Domain(
557 translation_directories=[
558 "/path/to/translations/with/messages/domain",
559 "/another/path/to/translations/with/another/domain",
560 ],
561 domains=[
562 "messages",
563 "myapp",
564 ]
565 )
566 """
567
568 def __init__(self, translation_directories=None, domain='messages'):
569 if isinstance(translation_directories, str):
570 translation_directories = [translation_directories]
571 self._translation_directories = translation_directories
572
573 self.domain = domain.split(';')
574
575 self.cache = {}
576
577 def __repr__(self):
578 return '<Domain({!r}, {!r})>'.format(
579 self._translation_directories,
580 self.domain
581 )
582
583 @property
584 def translation_directories(self):
585 if self._translation_directories is not None:
586 return self._translation_directories
587 return get_babel().translation_directories
588
589 def as_default(self):
590 """Set this domain as default for the current request"""
591 ctx = _get_current_context()
592
593 if ctx is None:
594 raise RuntimeError("No request context")
595
596 ctx.babel_domain = self
597
598 def get_translations_cache(self, ctx):
599 """Returns dictionary-like object for translation caching"""
600 return self.cache
601
602 def get_translations(self):
603 ctx = _get_current_context()
604
605 if ctx is None:
606 return support.NullTranslations()
607
608 cache = self.get_translations_cache(ctx)
609 locale = get_locale()
610 try:
611 return cache[str(locale), self.domain[0]]
612 except KeyError:
613 translations = support.Translations()
614
615 for index, dirname in enumerate(self.translation_directories):
616
617 domain = (
618 self.domain[0]
619 if len(self.domain) == 1
620 else self.domain[index]
621 )
622
623 catalog = support.Translations.load(
624 dirname,
625 [locale],
626 domain
627 )
628 translations.merge(catalog)
629 # FIXME: Workaround for merge() being really, really stupid. It
630 # does not copy _info, plural(), or any other instance variables
631 # populated by GNUTranslations. We probably want to stop using
632 # `support.Translations.merge` entirely.
633 if hasattr(catalog, 'plural'):
634 translations.plural = catalog.plural
635
636 cache[str(locale), self.domain[0]] = translations
637 return translations
638
639 def gettext(self, string, **variables):
640 """Translates a string with the current locale and passes in the
641 given keyword arguments as mapping to a string formatting string.
642
643 ::
644
645 gettext(u'Hello World!')
646 gettext(u'Hello %(name)s!', name='World')
647 """
648 t = self.get_translations()
649 s = t.ugettext(string)
650 return s if not variables else s % variables
651
652 def ngettext(self, singular, plural, num, **variables):
653 """Translates a string with the current locale and passes in the
654 given keyword arguments as mapping to a string formatting string.
655 The `num` parameter is used to dispatch between singular and various
656 plural forms of the message. It is available in the format string
657 as ``%(num)d`` or ``%(num)s``. The source language should be
658 English or a similar language which only has one plural form.
659
660 ::
661
662 ngettext(u'%(num)d Apple', u'%(num)d Apples', num=len(apples))
663 """
664 variables.setdefault('num', num)
665 t = self.get_translations()
666 s = t.ungettext(singular, plural, num)
667 return s if not variables else s % variables
668
669 def pgettext(self, context, string, **variables):
670 """Like :func:`gettext` but with a context.
671
672 .. versionadded:: 0.7
673 """
674 t = self.get_translations()
675 s = t.upgettext(context, string)
676 return s if not variables else s % variables
677
678 def npgettext(self, context, singular, plural, num, **variables):
679 """Like :func:`ngettext` but with a context.
680
681 .. versionadded:: 0.7
682 """
683 variables.setdefault('num', num)
684 t = self.get_translations()
685 s = t.unpgettext(context, singular, plural, num)
686 return s if not variables else s % variables
687
688 def lazy_gettext(self, string, **variables):
689 """Like :func:`gettext` but the string returned is lazy which means
690 it will be translated when it is used as an actual string.
691
692 Example::
693
694 hello = lazy_gettext(u'Hello World')
695
696 @app.route('/')
697 def index():
698 return unicode(hello)
699 """
700 return LazyString(self.gettext, string, **variables)
701
702 def lazy_ngettext(self, singular, plural, num, **variables):
703 """Like :func:`ngettext` but the string returned is lazy which means
704 it will be translated when it is used as an actual string.
705
706 Example::
707
708 apples = lazy_ngettext(
709 u'%(num)d Apple',
710 u'%(num)d Apples',
711 num=len(apples)
712 )
713
714 @app.route('/')
715 def index():
716 return unicode(apples)
717 """
718 return LazyString(self.ngettext, singular, plural, num, **variables)
719
720 def lazy_pgettext(self, context, string, **variables):
721 """Like :func:`pgettext` but the string returned is lazy which means
722 it will be translated when it is used as an actual string.
723
724 .. versionadded:: 0.7
725 """
726 return LazyString(self.pgettext, context, string, **variables)
727
728
729def _get_current_context() -> Optional[SimpleNamespace]:
730 if not g:
731 return None
732
733 if not hasattr(g, "_flask_babel"):
734 g._flask_babel = SimpleNamespace()
735
736 return g._flask_babel # noqa
737
738
739def get_domain() -> Domain:
740 ctx = _get_current_context()
741 if ctx is None:
742 # this will use NullTranslations
743 return Domain()
744
745 try:
746 return ctx.babel_domain
747 except AttributeError:
748 pass
749
750 ctx.babel_domain = get_babel().instance.domain_instance
751 return ctx.babel_domain
752
753
754# Create shortcuts for the default Flask domain
755def gettext(*args, **kwargs) -> str:
756 return get_domain().gettext(*args, **kwargs)
757
758
759_ = gettext
760
761
762def ngettext(*args, **kwargs) -> str:
763 return get_domain().ngettext(*args, **kwargs)
764
765
766def pgettext(*args, **kwargs) -> str:
767 return get_domain().pgettext(*args, **kwargs)
768
769
770def npgettext(*args, **kwargs) -> str:
771 return get_domain().npgettext(*args, **kwargs)
772
773
774def lazy_gettext(*args, **kwargs) -> LazyString:
775 return LazyString(gettext, *args, **kwargs)
776
777
778def lazy_pgettext(*args, **kwargs) -> LazyString:
779 return LazyString(pgettext, *args, **kwargs)
780
781
782def lazy_ngettext(*args, **kwargs) -> LazyString:
783 return LazyString(ngettext, *args, **kwargs)
784
785
786def lazy_npgettext(*args, **kwargs) -> LazyString:
787 return LazyString(npgettext, *args, **kwargs)