1# -*- coding: utf-8 -*-
2"""
3    webapp2_extras.i18n
4    ===================
5
6    Internationalization support for webapp2.
7
8    Several ideas borrowed from tipfy.i18n and Flask-Babel.
9
10    :copyright: 2011 by tipfy.org.
11    :license: Apache Sotware License, see LICENSE for details.
12"""
13import datetime
14import gettext as gettext_stdlib
15
16import babel
17from babel import dates
18from babel import numbers
19from babel import support
20
21try:
22    # Monkeypatches pytz for gae.
23    import pytz.gae
24except ImportError: # pragma: no cover
25    pass
26
27import pytz
28
29import webapp2
30
31#: Default configuration values for this module. Keys are:
32#:
33#: translations_path
34#:     Path to the translations directory. Default is `locale`.
35#:
36#: domains
37#:     List of gettext domains to be used. Default is ``['messages']``.
38#:
39#: default_locale
40#:     A locale code to be used as fallback. Default is ``'en_US'``.
41#:
42#: default_timezone
43#:     The application default timezone according to the Olson
44#:     database. Default is ``'UTC'``.
45#:
46#: locale_selector
47#:     A function that receives (store, request) and returns a locale
48#:     to be used for a request. If not defined, uses `default_locale`.
49#:     Can also be a string in dotted notation to be imported.
50#:
51#: timezone_selector
52#:     A function that receives (store, request) and returns a timezone
53#:     to be used for a request. If not defined, uses `default_timezone`.
54#:     Can also be a string in dotted notation to be imported.
55#:
56#: date_formats
57#:     Default date formats for datetime, date and time.
58default_config = {
59    'translations_path':   'locale',
60    'domains':             ['messages'],
61    'default_locale':      'en_US',
62    'default_timezone':    'UTC',
63    'locale_selector':     None,
64    'timezone_selector':   None,
65    'date_formats': {
66        'time':            'medium',
67        'date':            'medium',
68        'datetime':        'medium',
69        'time.short':      None,
70        'time.medium':     None,
71        'time.full':       None,
72        'time.long':       None,
73        'time.iso':        "HH':'mm':'ss",
74        'date.short':      None,
75        'date.medium':     None,
76        'date.full':       None,
77        'date.long':       None,
78        'date.iso':        "yyyy'-'MM'-'dd",
79        'datetime.short':  None,
80        'datetime.medium': None,
81        'datetime.full':   None,
82        'datetime.long':   None,
83        'datetime.iso':    "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ",
84    },
85}
86
87NullTranslations = gettext_stdlib.NullTranslations
88
89
90class I18nStore(object):
91    """Internalization store.
92
93    Caches loaded translations and configuration to be used between requests.
94    """
95
96    #: Configuration key.
97    config_key = __name__
98    #: A dictionary with all loaded translations.
99    translations = None
100    #: Path to where traslations are stored.
101    translations_path = None
102    #: Translation domains to merge.
103    domains = None
104    #: Default locale code.
105    default_locale = None
106    #: Default timezone code.
107    default_timezone = None
108    #: Dictionary of default date formats.
109    date_formats = None
110    #: A callable that returns the locale for a request.
111    locale_selector = None
112    #: A callable that returns the timezone for a request.
113    timezone_selector = None
114
115    def __init__(self, app, config=None):
116        """Initializes the i18n store.
117
118        :param app:
119            A :class:`webapp2.WSGIApplication` instance.
120        :param config:
121            A dictionary of configuration values to be overridden. See
122            the available keys in :data:`default_config`.
123        """
124        config = app.config.load_config(self.config_key,
125            default_values=default_config, user_values=config,
126            required_keys=None)
127        self.translations = {}
128        self.translations_path = config['translations_path']
129        self.domains = config['domains']
130        self.default_locale = config['default_locale']
131        self.default_timezone = config['default_timezone']
132        self.date_formats = config['date_formats']
133        self.set_locale_selector(config['locale_selector'])
134        self.set_timezone_selector(config['timezone_selector'])
135
136    def set_locale_selector(self, func):
137        """Sets the function that defines the locale for a request.
138
139        :param func:
140            A callable that receives (store, request) and returns the locale
141            for a request.
142        """
143        if func is None:
144            self.locale_selector = self.default_locale_selector
145        else:
146            if isinstance(func, basestring):
147                func = webapp2.import_string(func)
148
149            # Functions are descriptors, so bind it to this instance with
150            # __get__.
151            self.locale_selector = func.__get__(self, self.__class__)
152
153    def set_timezone_selector(self, func):
154        """Sets the function that defines the timezone for a request.
155
156        :param func:
157            A callable that receives (store, request) and returns the timezone
158            for a request.
159        """
160        if func is None:
161            self.timezone_selector = self.default_timezone_selector
162        else:
163            if isinstance(func, basestring):
164                func = webapp2.import_string(func)
165
166            self.timezone_selector = func.__get__(self, self.__class__)
167
168    def default_locale_selector(self, request):
169        return self.default_locale
170
171    def default_timezone_selector(self, request):
172        return self.default_timezone
173
174    def get_translations(self, locale):
175        """Returns a translation catalog for a locale.
176
177        :param locale:
178            A locale code.
179        :returns:
180            A ``babel.support.Translations`` instance, or
181            ``gettext.NullTranslations`` if none was found.
182        """
183        trans = self.translations.get(locale)
184        if not trans:
185            locales = (locale, self.default_locale)
186            trans = self.load_translations(self.translations_path, locales,
187                                           self.domains)
188            if not webapp2.get_app().debug:
189                self.translations[locale] = trans
190
191        return trans
192
193    def load_translations(self, dirname, locales, domains):
194        """Loads a translation catalog.
195
196        :param dirname:
197            Path to where translations are stored.
198        :param locales:
199            A list of locale codes.
200        :param domains:
201            A list of domains to be merged.
202        :returns:
203            A ``babel.support.Translations`` instance, or
204            ``gettext.NullTranslations`` if none was found.
205        """
206        trans = None
207        trans_null = None
208        for domain in domains:
209            _trans = support.Translations.load(dirname, locales, domain)
210            if isinstance(_trans, NullTranslations):
211                trans_null = _trans
212                continue
213            elif trans is None:
214                trans = _trans
215            else:
216                trans.merge(_trans)
217
218        return trans or trans_null or NullTranslations()
219
220
221class I18n(object):
222    """Internalization provider for a single request."""
223
224    #: A reference to :class:`I18nStore`.
225    store = None
226    #: The current locale code.
227    locale = None
228    #: The current translations.
229    translations = None
230    #: The current timezone code.
231    timezone = None
232    #: The current tzinfo object.
233    tzinfo = None
234
235    def __init__(self, request):
236        """Initializes the i18n provider for a request.
237
238        :param request:
239            A :class:`webapp2.Request` instance.
240        """
241        self.store = store = get_store(app=request.app)
242        self.set_locale(store.locale_selector(request))
243        self.set_timezone(store.timezone_selector(request))
244
245    def set_locale(self, locale):
246        """Sets the locale code for this request.
247
248        :param locale:
249            A locale code.
250        """
251        self.locale = locale
252        self.translations = self.store.get_translations(locale)
253
254    def set_timezone(self, timezone):
255        """Sets the timezone code for this request.
256
257        :param timezone:
258            A timezone code.
259        """
260        self.timezone = timezone
261        self.tzinfo = pytz.timezone(timezone)
262
263    def gettext(self, string, **variables):
264        """Translates a given string according to the current locale.
265
266        :param string:
267            The string to be translated.
268        :param variables:
269            Variables to format the returned string.
270        :returns:
271            The translated string.
272        """
273        if variables:
274            return self.translations.ugettext(string) % variables
275
276        return self.translations.ugettext(string)
277
278    def ngettext(self, singular, plural, n, **variables):
279        """Translates a possible pluralized string according to the current
280        locale.
281
282        :param singular:
283            The singular for of the string to be translated.
284        :param plural:
285            The plural for of the string to be translated.
286        :param n:
287            An integer indicating if this is a singular or plural. If greater
288            than 1, it is a plural.
289        :param variables:
290            Variables to format the returned string.
291        :returns:
292            The translated string.
293        """
294        if variables:
295            return self.translations.ungettext(singular, plural, n) % variables
296
297        return self.translations.ungettext(singular, plural, n)
298
299    def to_local_timezone(self, datetime):
300        """Returns a datetime object converted to the local timezone.
301
302        :param datetime:
303            A ``datetime`` object.
304        :returns:
305            A ``datetime`` object normalized to a timezone.
306        """
307        if datetime.tzinfo is None:
308            datetime = datetime.replace(tzinfo=pytz.UTC)
309
310        return self.tzinfo.normalize(datetime.astimezone(self.tzinfo))
311
312    def to_utc(self, datetime):
313        """Returns a datetime object converted to UTC and without tzinfo.
314
315        :param datetime:
316            A ``datetime`` object.
317        :returns:
318            A naive ``datetime`` object (no timezone), converted to UTC.
319        """
320        if datetime.tzinfo is None:
321            datetime = self.tzinfo.localize(datetime)
322
323        return datetime.astimezone(pytz.UTC).replace(tzinfo=None)
324
325    def _get_format(self, key, format):
326        """A helper for the datetime formatting functions. Returns a format
327        name or pattern to be used by Babel date format functions.
328
329        :param key:
330            A format key to be get from config. Valid values are "date",
331            "datetime" or "time".
332        :param format:
333            The format to be returned. Valid values are "short", "medium",
334            "long", "full" or a custom date/time pattern.
335        :returns:
336            A format name or pattern to be used by Babel date format functions.
337        """
338        if format is None:
339            format = self.store.date_formats.get(key)
340
341        if format in ('short', 'medium', 'full', 'long', 'iso'):
342            rv = self.store.date_formats.get('%s.%s' % (key, format))
343            if rv is not None:
344                format = rv
345
346        return format
347
348    def format_date(self, date=None, format=None, rebase=True):
349        """Returns a date formatted according to the given pattern and
350        following the current locale.
351
352        :param date:
353            A ``date`` or ``datetime`` object. If None, the current date in
354            UTC is used.
355        :param format:
356            The format to be returned. Valid values are "short", "medium",
357            "long", "full" or a custom date/time pattern. Example outputs:
358
359            - short:  11/10/09
360            - medium: Nov 10, 2009
361            - long:   November 10, 2009
362            - full:   Tuesday, November 10, 2009
363
364        :param rebase:
365            If True, converts the date to the current :attr:`timezone`.
366        :returns:
367            A formatted date in unicode.
368        """
369        format = self._get_format('date', format)
370
371        if rebase and isinstance(date, datetime.datetime):
372            date = self.to_local_timezone(date)
373
374        return dates.format_date(date, format, locale=self.locale)
375
376    def format_datetime(self, datetime=None, format=None, rebase=True):
377        """Returns a date and time formatted according to the given pattern
378        and following the current locale and timezone.
379
380        :param datetime:
381            A ``datetime`` object. If None, the current date and time in UTC
382            is used.
383        :param format:
384            The format to be returned. Valid values are "short", "medium",
385            "long", "full" or a custom date/time pattern. Example outputs:
386
387            - short:  11/10/09 4:36 PM
388            - medium: Nov 10, 2009 4:36:05 PM
389            - long:   November 10, 2009 4:36:05 PM +0000
390            - full:   Tuesday, November 10, 2009 4:36:05 PM World (GMT) Time
391
392        :param rebase:
393            If True, converts the datetime to the current :attr:`timezone`.
394        :returns:
395            A formatted date and time in unicode.
396        """
397        format = self._get_format('datetime', format)
398
399        kwargs = {}
400        if rebase:
401            kwargs['tzinfo'] = self.tzinfo
402
403        return dates.format_datetime(datetime, format, locale=self.locale,
404                                     **kwargs)
405
406    def format_time(self, time=None, format=None, rebase=True):
407        """Returns a time formatted according to the given pattern and
408        following the current locale and timezone.
409
410        :param time:
411            A ``time`` or ``datetime`` object. If None, the current
412            time in UTC is used.
413        :param format:
414            The format to be returned. Valid values are "short", "medium",
415            "long", "full" or a custom date/time pattern. Example outputs:
416
417            - short:  4:36 PM
418            - medium: 4:36:05 PM
419            - long:   4:36:05 PM +0000
420            - full:   4:36:05 PM World (GMT) Time
421
422        :param rebase:
423            If True, converts the time to the current :attr:`timezone`.
424        :returns:
425            A formatted time in unicode.
426        """
427        format = self._get_format('time', format)
428
429        kwargs = {}
430        if rebase:
431            kwargs['tzinfo'] = self.tzinfo
432
433        return dates.format_time(time, format, locale=self.locale, **kwargs)
434
435    def format_timedelta(self, datetime_or_timedelta, granularity='second',
436                         threshold=.85):
437        """Formats the elapsed time from the given date to now or the given
438        timedelta. This currently requires an unreleased development version
439        of Babel.
440
441        :param datetime_or_timedelta:
442            A ``timedelta`` object representing the time difference to format,
443            or a ``datetime`` object in UTC.
444        :param granularity:
445            Determines the smallest unit that should be displayed, the value
446            can be one of "year", "month", "week", "day", "hour", "minute" or
447            "second".
448        :param threshold:
449            Factor that determines at which point the presentation switches to
450            the next higher unit.
451        :returns:
452            A string with the elapsed time.
453        """
454        if isinstance(datetime_or_timedelta, datetime.datetime):
455            datetime_or_timedelta = datetime.datetime.utcnow() - \
456                datetime_or_timedelta
457
458        return dates.format_timedelta(datetime_or_timedelta, granularity,
459                                      threshold=threshold,
460                                      locale=self.locale)
461
462    def format_number(self, number):
463        """Returns the given number formatted for the current locale. Example::
464
465            >>> format_number(1099, locale='en_US')
466            u'1,099'
467
468        :param number:
469            The number to format.
470        :returns:
471            The formatted number.
472        """
473        return numbers.format_number(number, locale=self.locale)
474
475    def format_decimal(self, number, format=None):
476        """Returns the given decimal number formatted for the current locale.
477        Example::
478
479            >>> format_decimal(1.2345, locale='en_US')
480            u'1.234'
481            >>> format_decimal(1.2346, locale='en_US')
482            u'1.235'
483            >>> format_decimal(-1.2346, locale='en_US')
484            u'-1.235'
485            >>> format_decimal(1.2345, locale='sv_SE')
486            u'1,234'
487            >>> format_decimal(12345, locale='de')
488            u'12.345'
489
490        The appropriate thousands grouping and the decimal separator are used
491        for each locale::
492
493            >>> format_decimal(12345.5, locale='en_US')
494            u'12,345.5'
495
496        :param number:
497            The number to format.
498        :param format:
499            Notation format.
500        :returns:
501            The formatted decimal number.
502        """
503        return numbers.format_decimal(number, format=format,
504                                      locale=self.locale)
505
506    def format_currency(self, number, currency, format=None):
507        """Returns a formatted currency value. Example::
508
509            >>> format_currency(1099.98, 'USD', locale='en_US')
510            u'$1,099.98'
511            >>> format_currency(1099.98, 'USD', locale='es_CO')
512            u'US$\\xa01.099,98'
513            >>> format_currency(1099.98, 'EUR', locale='de_DE')
514            u'1.099,98\\xa0\\u20ac'
515
516        The pattern can also be specified explicitly::
517
518            >>> format_currency(1099.98, 'EUR', u'\\xa4\\xa4 #,##0.00',
519            ...                 locale='en_US')
520            u'EUR 1,099.98'
521
522        :param number:
523            The number to format.
524        :param currency:
525            The currency code.
526        :param format:
527            Notation format.
528        :returns:
529            The formatted currency value.
530        """
531        return numbers.format_currency(number, currency, format=format,
532                                       locale=self.locale)
533
534    def format_percent(self, number, format=None):
535        """Returns formatted percent value for the current locale. Example::
536
537            >>> format_percent(0.34, locale='en_US')
538            u'34%'
539            >>> format_percent(25.1234, locale='en_US')
540            u'2,512%'
541            >>> format_percent(25.1234, locale='sv_SE')
542            u'2\\xa0512\\xa0%'
543
544        The format pattern can also be specified explicitly::
545
546            >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US')
547            u'25,123\u2030'
548
549        :param number:
550            The percent number to format
551        :param format:
552            Notation format.
553        :returns:
554            The formatted percent number.
555        """
556        return numbers.format_percent(number, format=format,
557                                      locale=self.locale)
558
559    def format_scientific(self, number, format=None):
560        """Returns value formatted in scientific notation for the current
561        locale. Example::
562
563            >>> format_scientific(10000, locale='en_US')
564            u'1E4'
565
566        The format pattern can also be specified explicitly::
567
568            >>> format_scientific(1234567, u'##0E00', locale='en_US')
569            u'1.23E06'
570
571        :param number:
572            The number to format.
573        :param format:
574            Notation format.
575        :returns:
576            Value formatted in scientific notation.
577        """
578        return numbers.format_scientific(number, format=format,
579                                         locale=self.locale)
580
581    def parse_date(self, string):
582        """Parses a date from a string.
583
584        This function uses the date format for the locale as a hint to
585        determine the order in which the date fields appear in the string.
586        Example::
587
588            >>> parse_date('4/1/04', locale='en_US')
589            datetime.date(2004, 4, 1)
590            >>> parse_date('01.04.2004', locale='de_DE')
591            datetime.date(2004, 4, 1)
592
593        :param string:
594            The string containing the date.
595        :returns:
596            The parsed date object.
597        """
598        return dates.parse_date(string, locale=self.locale)
599
600    def parse_datetime(self, string):
601        """Parses a date and time from a string.
602
603        This function uses the date and time formats for the locale as a hint
604        to determine the order in which the time fields appear in the string.
605
606        :param string:
607            The string containing the date and time.
608        :returns:
609            The parsed datetime object.
610        """
611        return dates.parse_datetime(string, locale=self.locale)
612
613    def parse_time(self, string):
614        """Parses a time from a string.
615
616        This function uses the time format for the locale as a hint to
617        determine the order in which the time fields appear in the string.
618        Example::
619
620            >>> parse_time('15:30:00', locale='en_US')
621            datetime.time(15, 30)
622
623        :param string:
624            The string containing the time.
625        :returns:
626            The parsed time object.
627        """
628        return dates.parse_time(string, locale=self.locale)
629
630    def parse_number(self, string):
631        """Parses localized number string into a long integer. Example::
632
633            >>> parse_number('1,099', locale='en_US')
634            1099L
635            >>> parse_number('1.099', locale='de_DE')
636            1099L
637
638        When the given string cannot be parsed, an exception is raised::
639
640            >>> parse_number('1.099,98', locale='de')
641            Traceback (most recent call last):
642               ...
643            NumberFormatError: '1.099,98' is not a valid number
644
645        :param string:
646            The string to parse.
647        :returns:
648            The parsed number.
649        :raises:
650            ``NumberFormatError`` if the string can not be converted to a
651            number.
652        """
653        return numbers.parse_number(string, locale=self.locale)
654
655    def parse_decimal(self, string):
656        """Parses localized decimal string into a float. Example::
657
658            >>> parse_decimal('1,099.98', locale='en_US')
659            1099.98
660            >>> parse_decimal('1.099,98', locale='de')
661            1099.98
662
663        When the given string cannot be parsed, an exception is raised::
664
665            >>> parse_decimal('2,109,998', locale='de')
666            Traceback (most recent call last):
667               ...
668            NumberFormatError: '2,109,998' is not a valid decimal number
669
670        :param string:
671            The string to parse.
672        :returns:
673            The parsed decimal number.
674        :raises:
675            ``NumberFormatError`` if the string can not be converted to a
676            decimal number.
677        """
678        return numbers.parse_decimal(string, locale=self.locale)
679
680    def get_timezone_location(self, dt_or_tzinfo):
681        """Returns a representation of the given timezone using "location
682        format".
683
684        The result depends on both the local display name of the country and
685        the city assocaited with the time zone::
686
687            >>> from pytz import timezone
688            >>> tz = timezone('America/St_Johns')
689            >>> get_timezone_location(tz, locale='de_DE')
690            u"Kanada (St. John's)"
691            >>> tz = timezone('America/Mexico_City')
692            >>> get_timezone_location(tz, locale='de_DE')
693            u'Mexiko (Mexiko-Stadt)'
694
695        If the timezone is associated with a country that uses only a single
696        timezone, just the localized country name is returned::
697
698            >>> tz = timezone('Europe/Berlin')
699            >>> get_timezone_name(tz, locale='de_DE')
700            u'Deutschland'
701
702        :param dt_or_tzinfo:
703            The ``datetime`` or ``tzinfo`` object that determines
704            the timezone; if None, the current date and time in UTC is assumed.
705        :returns:
706            The localized timezone name using location format.
707        """
708        return dates.get_timezone_name(dt_or_tzinfo, locale=self.locale)
709
710
711def gettext(string, **variables):
712    """See :meth:`I18n.gettext`."""
713    return get_i18n().gettext(string, **variables)
714
715
716def ngettext(singular, plural, n, **variables):
717    """See :meth:`I18n.ngettext`."""
718    return get_i18n().ngettext(singular, plural, n, **variables)
719
720
721def to_local_timezone(datetime):
722    """See :meth:`I18n.to_local_timezone`."""
723    return get_i18n().to_local_timezone(datetime)
724
725
726def to_utc(datetime):
727    """See :meth:`I18n.to_utc`."""
728    return get_i18n().to_utc(datetime)
729
730
731def format_date(date=None, format=None, rebase=True):
732    """See :meth:`I18n.format_date`."""
733    return get_i18n().format_date(date, format, rebase)
734
735
736def format_datetime(datetime=None, format=None, rebase=True):
737    """See :meth:`I18n.format_datetime`."""
738    return get_i18n().format_datetime(datetime, format, rebase)
739
740
741def format_time(time=None, format=None, rebase=True):
742    """See :meth:`I18n.format_time`."""
743    return get_i18n().format_time(time, format, rebase)
744
745
746def format_timedelta(datetime_or_timedelta, granularity='second',
747    threshold=.85):
748    """See :meth:`I18n.format_timedelta`."""
749    return get_i18n().format_timedelta(datetime_or_timedelta,
750                                       granularity, threshold)
751
752
753def format_number(number):
754    """See :meth:`I18n.format_number`."""
755    return get_i18n().format_number(number)
756
757
758def format_decimal(number, format=None):
759    """See :meth:`I18n.format_decimal`."""
760    return get_i18n().format_decimal(number, format)
761
762
763def format_currency(number, currency, format=None):
764    """See :meth:`I18n.format_currency`."""
765    return get_i18n().format_currency(number, currency, format)
766
767
768def format_percent(number, format=None):
769    """See :meth:`I18n.format_percent`."""
770    return get_i18n().format_percent(number, format)
771
772
773def format_scientific(number, format=None):
774    """See :meth:`I18n.format_scientific`."""
775    return get_i18n().format_scientific(number, format)
776
777
778def parse_date(string):
779    """See :meth:`I18n.parse_date`"""
780    return get_i18n().parse_date(string)
781
782
783def parse_datetime(string):
784    """See :meth:`I18n.parse_datetime`."""
785    return get_i18n().parse_datetime(string)
786
787
788def parse_time(string):
789    """See :meth:`I18n.parse_time`."""
790    return get_i18n().parse_time(string)
791
792
793def parse_number(string):
794    """See :meth:`I18n.parse_number`."""
795    return get_i18n().parse_number(string)
796
797
798def parse_decimal(string):
799    """See :meth:`I18n.parse_decimal`."""
800    return get_i18n().parse_decimal(string)
801
802
803def get_timezone_location(dt_or_tzinfo):
804    """See :meth:`I18n.get_timezone_location`."""
805    return get_i18n().get_timezone_location(dt_or_tzinfo)
806
807
808def lazy_gettext(string, **variables):
809    """A lazy version of :func:`gettext`.
810
811    :param string:
812        The string to be translated.
813    :param variables:
814        Variables to format the returned string.
815    :returns:
816        A ``babel.support.LazyProxy`` object that when accessed translates
817        the string.
818    """
819    return support.LazyProxy(gettext, string, **variables)
820
821
822# Aliases.
823_ = gettext
824_lazy = lazy_gettext
825
826
827# Factories -------------------------------------------------------------------
828
829
830#: Key used to store :class:`I18nStore` in the app registry.
831_store_registry_key = 'webapp2_extras.i18n.I18nStore'
832#: Key used to store :class:`I18n` in the request registry.
833_i18n_registry_key = 'webapp2_extras.i18n.I18n'
834
835
836def get_store(factory=I18nStore, key=_store_registry_key, app=None):
837    """Returns an instance of :class:`I18nStore` from the app registry.
838
839    It'll try to get it from the current app registry, and if it is not
840    registered it'll be instantiated and registered. A second call to this
841    function will return the same instance.
842
843    :param factory:
844        The callable used to build and register the instance if it is not yet
845        registered. The default is the class :class:`I18nStore` itself.
846    :param key:
847        The key used to store the instance in the registry. A default is used
848        if it is not set.
849    :param app:
850        A :class:`webapp2.WSGIApplication` instance used to store the instance.
851        The active app is used if it is not set.
852    """
853    app = app or webapp2.get_app()
854    store = app.registry.get(key)
855    if not store:
856        store = app.registry[key] = factory(app)
857
858    return store
859
860
861def set_store(store, key=_store_registry_key, app=None):
862    """Sets an instance of :class:`I18nStore` in the app registry.
863
864    :param store:
865        An instance of :class:`I18nStore`.
866    :param key:
867        The key used to retrieve the instance from the registry. A default
868        is used if it is not set.
869    :param request:
870        A :class:`webapp2.WSGIApplication` instance used to retrieve the
871        instance. The active app is used if it is not set.
872    """
873    app = app or webapp2.get_app()
874    app.registry[key] = store
875
876
877def get_i18n(factory=I18n, key=_i18n_registry_key, request=None):
878    """Returns an instance of :class:`I18n` from the request registry.
879
880    It'll try to get it from the current request registry, and if it is not
881    registered it'll be instantiated and registered. A second call to this
882    function will return the same instance.
883
884    :param factory:
885        The callable used to build and register the instance if it is not yet
886        registered. The default is the class :class:`I18n` itself.
887    :param key:
888        The key used to store the instance in the registry. A default is used
889        if it is not set.
890    :param request:
891        A :class:`webapp2.Request` instance used to store the instance. The
892        active request is used if it is not set.
893    """
894    request = request or webapp2.get_request()
895    i18n = request.registry.get(key)
896    if not i18n:
897        i18n = request.registry[key] = factory(request)
898
899    return i18n
900
901
902def set_i18n(i18n, key=_i18n_registry_key, request=None):
903    """Sets an instance of :class:`I18n` in the request registry.
904
905    :param store:
906        An instance of :class:`I18n`.
907    :param key:
908        The key used to retrieve the instance from the registry. A default
909        is used if it is not set.
910    :param request:
911        A :class:`webapp2.Request` instance used to retrieve the instance. The
912        active request is used if it is not set.
913    """
914    request = request or webapp2.get_request()
915    request.registry[key] = i18n
916