Search code examples
pythonurl-rewritinginternationalizationpyramid

How to add language code to URL in Pyramid?


I've just created the basic Pyramid "hello world" template project and added i18n support. I'm using Python 3.5 and Chameleon templates (.pt) with gettext.

I can currently change the language through the .ini file.

Now I would like to make it dynamic and read the language code from the URL. So URLs are changed to /<language code>/page/{possible params} so for example /fi/home. I don't want to add {language} to existing routes/views so that the language code parameter is hidden and views don't know anything about it except when creating links to other pages in templates/views.

EDIT: Here's my attempt using tweens mentioned by Mikko Ohtamaa:

Added to __init__.py:

config.add_tween('myapp.tweens.LocalizerTween')

tweens.py:

import logging

from pyramid.registry import Registry
from pyramid.request import Request

log = logging.getLogger(__name__)


class LocalizerTween(object):
  """
  Set translator based on URL
  """
  def __init__(self, handler, registry: Registry):
    self.handler = handler
    self.registry = registry

  def __call__(self, request: Request):
    if request.path.count("/") > 1 and len(request.path) > 3:
      request.locale_name = request.path[1:].split("/", 1)[0]
    else:
      # Redirect to default language
      from pyramid.settings import aslist
      import pyramid.httpexceptions as exc
      raise exc.HTTPFound("/" + aslist(request.registry.settings['pyramid.default_locale_name'])[0] + "/")

    newpath = request.path[1:]
    newpath = newpath[newpath.find("/"):]
    log.debug("new path: %s", newpath)

    request.path = newpath

    response = self.handler(request)
    return response

Redirect to default language gives exception:

pyramid.httpexceptions.HTTPFound: The resource was found at

Trying to set new path gives:

AttributeError: can't set attribute

If I comment out request.path = newpath and go to /fi/ and /en/ I get 404 page in correct language.


Solution

  • Here is an example solution, strictly limited to the scope of language aware paths (no localization bindings, etc):

    """Self-contained language aware path routing example for Pyramid."""
    
    from urllib.parse import urlunparse
    from urllib.parse import urlparse
    from wsgiref.simple_server import make_server
    
    from pyramid.config import Configurator
    from pyramid.httpexceptions import HTTPFound
    from pyramid.request import Request
    from pyramid.response import Response
    
    
    def redirect_to_default_language(request: Request):
        """A view that redirects path language-free URLs to the default language URLs.
    
        E.g. /greeter/foobar -> /en/greeter/foobar
        """
    
        default_language = request.registry.settings["default_language"]
    
        parts = urlparse(request.url)
        new_path = "/{}{}".format(default_language, parts.path)
        new_parts = [parts[0], parts[1], new_path, parts[3], parts[4], parts[5]]
        language_redirected_url = urlunparse(new_parts)
        return HTTPFound(language_redirected_url)
    
    
    def add_localized_route(config, name, pattern, factory=None, pregenerator=None, **kw):
        """Create path language aware routing paths.
    
        Each route will have /{lang}/ prefix added to them.
    
        Optionally, if default language is set, we'll create redirect from an URL without language path component to the URL with the language path component.
        """
        orig_factory = factory
    
        def wrapper_factory(request):
            lang = request.matchdict['lang']
            # determine if this is a supported lang and convert it to a locale,
            # likely defaulting to your default language if the requested one is
            # not supported by your app
            request.path_lang = lang
    
            if orig_factory:
                return orig_factory(request)
    
        orig_pregenerator = pregenerator
    
        def wrapper_pregenerator(request, elements, kw):
            if 'lang' not in kw:
                # not quite right but figure out how to convert request._LOCALE_ back into a language url
                kw['lang'] = request.path_lang
            if orig_pregenerator:
                return orig_pregenerator(elements, kw)
            return elements, kw
    
        if pattern.startswith('/'):
            new_pattern = pattern[1:]
        else:
            new_pattern = pattern
    
        new_pattern = '/{lang}/' + new_pattern
    
        # Language-aware URL routed
        config.add_route(name, new_pattern, factory=wrapper_factory, pregenerator=wrapper_pregenerator, **kw)
    
        # Add redirect to the default language routes
        if config.registry.settings.get("default_language"):
            # TODO: This works only for the most simplest routes
            fallback_route_name = name + "_language_redirect_fallback"
            config.add_route(fallback_route_name, pattern)
            config.add_view(redirect_to_default_language, route_name=fallback_route_name)
    
    
    def home(request):
        """Example of language aware parameterless routing."""
    
        if request.path_lang == "fi":
            msg = 'Hyvää päivää!'
        else:
            msg = 'Hello sir'
    
        # This will use current language
        # and automatically populate /{lang}/ matchdict
        # as in wrapper_pregenerator()
        another_url = request.route_url("greeter", name="mikko")
    
        text = """{}
    
        Also see {}
        """.format(msg, another_url)
    
        return Response(text)
    
    
    def greeter(request):
        """Example of language aware matchdict routing."""
        name = request.matchdict["name"]
        if request.path_lang == "fi":
            return Response('Mitä kuuluu {}?'.format(name))
        else:
            return Response('How are you {}?'.format(name))
    
    
    if __name__ == '__main__':
        config = Configurator()
    
        # Map all /lang/ free URLs to this language
        config.registry.settings["default_language"] = "en"
    
        # Set up config.add_localized_route
        config.add_directive('add_localized_route', add_localized_route)
    
        # Parameterless routing
        # This will create
        # -  /
        # - /fi/
        # - /en/
        # patterns
        config.add_localized_route('home', '/')
        config.add_view(home, route_name='home')
    
        # Match dict routing
        # This will create
        # - /greet/mikko
        # - /en/greet/mikko
        # - /fi/greet/mikko
        # patterns
        config.add_localized_route('greeter', '/greet/{name}')
        config.add_view(greeter, route_name='greeter')
    
        # Run the web server
        app = config.make_wsgi_app()
        server = make_server('0.0.0.0', 8080, app)
        server.serve_forever()