Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

file 283 lines (220 sloc) 10.374 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
# -*- coding: utf-8 -*-

'''A little plugin supporting human language alternation in Pyramid.
It lets you enable and disable locales (as your translations allow)
in the configuration file.

This module also offers *BaseLocalizedView*, a useful mixin class for
your application's base view.

Enabling the module in 2 steps
------------------------------

1) Add a setting to your .ini file with the locales you want to enable,
such as::

bag.locale.enable = en pt_BR es de fr

2) Add to your initialization file::

config.include('bag.web.pyramid.locale')

Effects of enabling this module as described above
--------------------------------------------------

1) A view is registered so the user can, for instance, browse to
/locale/pt_BR
in order to change the locale to Brazilian Portuguese.
This works by setting the locale cookie.

2) To improve the experience of first-time visitors, a locale negotiator
is registered that checks the browser's stated preferred languages
against the "bag.locale.enable" setting in the .ini file.

3) The "bag.locale.enable" setting
is replaced with an OrderedDictionary that is useful
for you to build a user interface for locale choice.

4) In templates, that dictionary is available as *enabled_locales*.

For instance, you might do this in your template to list the
available locales so the user can click and change languages:

.. code-block:: html

<span class='languages-selector'
py:with="locales = enabled_locales.values()">
<py:for each="loc in locales">
<a href="${url('locale', locale=loc.code)}"
title="${loc.title}"
py:strip="locale_code == loc.code"
py:content="loc.code[:2]" />
<span py:if="not loc is locales[-1]" class="separator">|</span>
</py:for>
</span>

5) In your master template, you can set the 2 lang attributes
on the <html> tag::

xml:lang="${locale_code[:2]}" lang="${locale_code[:2]}"

...because the variable ``locale_code`` is made available to
template context.
'''

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
try:
    from collections import OrderedDict
except ImportError: # It's time people using Python 2.6 would upgrade already
    from ordereddict import OrderedDict
from babel import Locale
from babel.numbers import format_number, format_currency
from pyramid.httpexceptions import HTTPFound
from pyramid.i18n import get_locale_name, default_locale_negotiator
from nine import basestring, native_str, iterkeys, iteritems, nine
from . import _

SETTING_NAME = 'bag.locale.enable'


@nine
class LocaleInfo(object):
    def __init__(self, code, display_name, english_name, title=None,
                 babel_locale=None):
        self.code = code
        self.display_name = display_name
        self.english_name = english_name
        self.title = title
        self.babel_locale = babel_locale

    def __repr__(self):
        return self.code

    def __str__(self):
        return self.display_name


locale_titles = dict(
    en='Change to English',
    en_US='Change to English - United States',
    en_DEV='Change to strings as in the source code',
    pt='Mudar para português',
    pt_BR='Mudar para português do Brasil',
    es='Cambiar a español',
    de='Auf Deutsch benutzen',
) # Please help us: send more entries to this dict


class LocaleDict(OrderedDict):
    '''A dictionary of dictionaries grouping:

* locale codes (i.e. "pt_BR"),
* human-readable and translatable language names such as "English", and
* titles such as "Change to English", written in THAT language.
'''
    def add(self, code, display_name=None, english_name=None, title=None):
        babel_locale = Locale(*code.split("_"))
        self[code] = LocaleInfo(code,
            display_name=display_name or babel_locale.display_name,
            english_name=english_name or babel_locale.english_name,
            title=title or locale_titles.get(code),
            babel_locale=babel_locale)


def prepare_enabled_locales(settings, Dict=LocaleDict):
    '''If you have a *bag.locale.enable* setting with a value such as
"en pt-BR es fr de" (without the quotes),
this method substitutes that setting with
a dictionary of Locale objects which is useful to
build a web interface for the user to change the locale.
'''
    # Read from settings a list of locale codes that should be enabled
    codes = set(settings.get(SETTING_NAME, 'en').split(' '))
    # Create the LocaleDict containing info for each code
    langs = Dict()
    for code in codes:
        langs.add(code)
    # Replace the setting with the dictionary, containing more information
    settings[SETTING_NAME] = langs
    return langs


def locale_cookie_headers(locale_code):
    '''Returns HTTP headers setting the cookie that stores the
Pyramid locale.
'''
    # native_str() calls below are because waitress expects
    # bytes in Python 2 and unicode in Python 3.
    return [(native_str('Set-Cookie'), native_str('_LOCALE_={0}; expires='
        'Fri, 31-Dec-9999 23:00:00 GMT; Path=/'.format(locale_code)))]


def locale_view(request):
    '''View that sets the locale cookie -- as long as the requested locale
is enabled -- and redirects back to the referer.
'''
    locale_code = request.matchdict['locale']
    # Ensure this locale code is one of the enabled_locales
    if not locale_code in request.registry.settings[SETTING_NAME]:
        raise KeyError('Locale not enabled: "{0}"'.format(locale_code))
    return HTTPFound(location=request.referrer or '/',
        headers=locale_cookie_headers(locale_code))


def locale_from_browser(request):
    '''The browser provides the user's preferred languages, ordered.
They come in the *Accept-Language* header.

This function returns the best match between that list and
the enabled languages.

Most of the implementation is in the WebOb request object.
'''
    # settings = request.registry.settings
    # enabled_locales = settings[SETTING_NAME].keys()
    # first = enabled_locales[0]
    # return request.accept_language.best_match(enabled_locales,
    # default_match=settings.get("pyramid.default_locale_name", first))
    return request.accept_language.best_match(iterkeys(
        request.registry.settings[SETTING_NAME]))


def locale_negotiator(request):
    '''This is a locale negotiator that decorates Pyramid's default one.

If the default locale negotiator's schemes should fail,
we try to match the browser's stated preferred languages
with our configured enabled locales.
'''
    return default_locale_negotiator(request) or locale_from_browser(request)


def add_template_globals(event):
    '''Makes the following variables readily available in template context:

* *enabled_locales*: OrderedDict containing the enabled locales
* *locale_code*: a string containing the locale of the current request,
which you can use to set the xml:lang attribute on your <html> tag.
'''
    event['enabled_locales'] = \
        event['request'].registry.settings[SETTING_NAME]
    event['locale_code'] = get_locale_name(event['request'])


def includeme(config):
    if hasattr(config, 'bag_locale_included'):
        return # Include only once per config
    config.bag_locale_included = True

    prepare_enabled_locales(config.get_settings())
    config.set_locale_negotiator(locale_negotiator)
    config.add_route('locale', 'locale/{locale}')
    config.add_view(locale_view, route_name='locale')
    from pyramid.interfaces import IBeforeRender
    config.add_subscriber(add_template_globals, IBeforeRender)


class BaseLocalizedView(object):
    '''A mixin class for your application's base view class.'''
    def format_number(self, n):
        return format_number(n, locale=get_locale_name(self.request))

    def format_currency(self, n, currency=None, format=None):
        return format_currency(n, format=format,
            currency=currency or getattr(self, 'default_currency', 'USD'),
            locale=get_locale_name(self.request))


def sorted_countries(arg, top_entry=True): # TODO memoized version
    '''*arg* may be either the desired locale code or the request object,
from which the locale will be discovered.

Returns a list of tuples like ``('BR', 'Brazil')``, already sorted,
ready for inclusion in your web form.
'''
    code = arg if isinstance(arg, basestring) else get_locale_name(arg)

    def generator(territories):
        if top_entry:
            yield (native_str(''), _("- Choose -")) # TODO translate somehow
        for tup in territories:
            if len(tup[0]) == 2: # Keep only countries
                yield tup
    return sorted(generator(iteritems(Locale(code).territories)),
        key=lambda x: x[1])


# Colander and Deform section
# ===========================

def locale_exists_validator(settings):
    '''If you use Deform or even just Colander, you can use this to get a
validator that you can use on your user preferences form,
so a user can only choose a locale that is enabled in the system.

TODO: Test this new implementation when I use colander again...
'''
    from colander import Invalid
    enabled_locales = settings[SETTING_NAME]

    def validator(node, value):
        if value not in enabled_locales:
            raise Invalid(node, _('Please select a language.'))
    return validator


def language_dropdown(settings, title=_('Locale'), name='locale',
                      blank_option_at_top=True):
    '''If you use Deform, you can use this to get a SchemaNode that lets
the user select a locale from the enabled ones.
'''
    import colander as c
    import deform as d

    def options():
        if blank_option_at_top:
            yield ('', _('- Choose -'))
        for loc in settings[SETTING_NAME].values():
            yield (loc.code, loc.display_name)
    values = sorted(options(), key=lambda t: _(t[1]))
    return c.SchemaNode(c.Str(), title=title, name=name,
                        validator=locale_exists_validator(settings),
                        widget=d.widget.SelectWidget(values=values))
Something went wrong with that request. Please try again.