Pyramid Internationalization HowTo
Recently I started studying Pyramid, a Python web application development framework. Although the excellent documentation published in the Pylons Project website, I found it difficult to learn how to translate my web application in other languages. This was mostly due because I couldn't find a comprehensive tutorial that explains it all, but I found several resources that help me out to understand each bit that I needed.
After a while of doing experiments I came up with the pyramid_i18n_howto Git repository and I decided to write this tutorial which helps you to learn step by step how to implement i18n into your web application.
This is a practical how to, so I'm not going in deep with any explanation but I invite you to read the references I used in order to have a better understanding of Pyramid internationalization and localization.
1. Creating a Pyramid Project
We start off with a new Pyramid project [1], for this you will need Python and Pyramid installed, see Installing Pyramid for the details:
(env)$ pcreate -t starter pyramid_i18n_howto (env)$ cd pyramid_i18n_howto/ (env)$ python setup.py develop (env)$ python setup.py test -q (env)$ pserve development.ini
Browse to your project by visiting http://localhost:6543 in your browser.
2. Setup Internationalization and Localization
To setup Internationalization and Localization [2] you need to install Babel and lingua packages in your virtual environment:
(env)$ easy_install Babel lingua
Then edit the setup.py file in order to generate gettext files from your application.
In particular, add the Babel and lingua distributions to the requires list and insert a set of references to Babel message extractors:
# ... requires = [ # ... 'Babel', 'lingua', ] setup(name="mypackage", # ... message_extractors = { '.': [ ('**.py', 'lingua_python', None ), ('**.pt', 'lingua_xml', None ), ]}, )
The message_extractors stanza placed into the setup.py file causes the Babel message catalog extraction machinery to also consider *.pt files when doing message id extraction.
If you use Mako templates you may also want to add the following in the message_extractors [3]:
# ... message_extractors = { '.': [ # ... ('templates/**.html', 'mako', None), ('templates/**.mako', 'mako', None), ('static/**', 'ignore', None) ]},
Then you need to add your locale directory to your project’s configuration, edit pyramid_i18n_howto/__init__.py file:
def main(...): # ... config.add_translation_dirs('pyramid_i18n_howto:locale')
3. Mark Messages for Translation
Now it's time to mark some messages that need to be translated in our template, but first add a namespace and the i18n:domain to the templates/mytemplate.pt template file:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal" xmlns:i18n="http://xml.zope.org/namespaces/i18n" i18n:domain="pyramid_i18n_howto">
Then we mark some messages for translation, for simplicity we edit only the Search documentation string:
<h2 i18n:translate="search_documentation">Search documentation</h2>
4. Extracting Messages from a Template
We follow by extracting the messages from a template, run these commands in your project’s directory:
(env)$ mkdir pyramid_i18n_howto/locale (env)$ python setup.py extract_messages
Last command it creates the message catalog template named pyramid_i18n_howto/locale/pyramid_i18n_howto.pot.
5. Initializing the Message Catalog Files
Then initialize the catalogs for each language that you want to use:
(env)$ python setup.py init_catalog -l en (env)$ python setup.py init_catalog -l es (env)$ python setup.py init_catalog -l it
The message catalogs .po files will end up in:
pyramid_i18n_howto/locale/en/LC_MESSAGES/pyramid_i18n_howto.po pyramid_i18n_howto/locale/es/LC_MESSAGES/pyramid_i18n_howto.po pyramid_i18n_howto/locale/it/LC_MESSAGES/pyramid_i18n_howto.po
Once the files are there, they can be worked on by a human translator. One tool that may help you with this is Poedit.
6. Compiling the Message Catalog Files
Pyramid itself ignores the existence of all .po files. For a running application to have translations available, you need to compile the catalogs to .mo files.
Once your catalog files have been translated, run the following command:
(env)$ python setup.py compile_catalog
- Note
- I usually include .mo files in the .gitignore to keep them out of the version control system as they are just binaries. I added them here just for completeness of this tutorial.
7. Define the Default Local Name
We are now able to see our web application translated, so define your default locale name in the development.ini file:
[app:main] #... pyramid.default_locale_name = it #...
Run the application using the pserve command:
(env)$ pserve development.ini
Visit http://localhost:6543 in your browser, you should see your messages translated.
8. Translating Strings in the Python Code
We learn how to translate strings in template files, but we also need to translate strings inside our Python code and for that we need to use a localizer [4], a TranslationStringFactory and to add a renderer globals [5] (which is currently deprecated as of Pyramid 1.1, so I will have to investigate more on this) .
You can use the pyramid.i18n.get_localizer() function to obtain a localizer.
Create the following pyramid_i18n_howto/i18n.py file:
from pyramid.i18n import get_localizer, TranslationStringFactory def add_renderer_globals(event): request = event.get('request') if request is None: request = get_current_request() event['_'] = request.translate event['localizer'] = request.localizer tsf = TranslationStringFactory('pyramid_i18n_howto') def add_localizer(event): request = event.request localizer = get_localizer(request) def auto_translate(string): return localizer.translate(tsf(string)) request.localizer = localizer request.translate = auto_translate
Then we change the application configuration by adding the following event subscribers [6]:
config.add_subscriber('pyramid_i18n_howto.i18n.add_renderer_globals', 'pyramid.events.BeforeRender') config.add_subscriber('pyramid_i18n_howto.i18n.add_localizer', 'pyramid.events.NewRequest')
Now mark a string for translation in the view.py module, replace the following line:
return {'project': 'pyramid_i18n_howto'}
with:
_ = request.translate return {'project': _('My i18n project')}
here we used the _() function, which is a convenient way of marking translations strings.
9. Updating the catalog files
As we added another translation string, we need to extract again the messages to the catalog template and update our catalog files:
(env)$ python setup.py extract_messages (env)$ python setup.py update_catalog
Once again a human translator have to translate the messages, and don't forget to recompile the catalogs files:
(env)$ python setup.py compile_catalog
Test your application by running it with pserve command and visit http://localhost:6543 in your browser, you should be able to read your messages translated into the language defined by pyramid.default_locale_name.
10. Determine the User Language
When developing a web application, you may want to determine the user language. Pyramid doesn't dictate how a locale should be negotiated, one way to do it is basing your site language on the Accept-Language header [7].
Add the following code to i18n.py module:
from pyramid.events import NewRequest from pyramid.events import subscriber from webob.acceptparse import Accept @subscriber(NewRequest) def setAcceptedLanguagesLocale(event): if not event.request.accept_language: return accepted = event.request.accept_language event.request._LOCALE_ = accepted.best_match(('en', 'es', 'it'), 'it')
11. Using a Custom Locale Negotiator
Most of the web applications can make use of the default locale negotiator [8], which requires no additional coding or configuration.
Sometimes, the default locale negotiation scheme doesn't fit our web application needs and it's better to create a custom one.
As an example we modify the original default_locale_negotiator() by implementing the check for the Accept-Language header.
Add the custom_locale_negotiator() function to the i18n.py module:
LOCALES = ('en', 'es', 'it') def custom_locale_negotiator(request): """ The :term:`custom locale negotiator`. Returns a locale name. - First, the negotiator looks for the ``_LOCALE_`` attribute of the request object (possibly set by a view or a listener for an :term:`event`). - Then it looks for the ``request.params['_LOCALE_']`` value. - Then it looks for the ``request.cookies['_LOCALE_']`` value. - Then it looks for the ``Accept-Language`` header value, which is set by the user in his/her browser configuration. - Finally, if the locale could not be determined via any of the previous checks, the negotiator returns the :term:`default locale name`. """ name = '_LOCALE_' locale_name = getattr(request, name, None) if locale_name is None: locale_name = request.params.get(name) if locale_name is None: locale_name = request.cookies.get(name) if locale_name is None: locale_name = request.accept_language.best_match( LOCALES, request.registry.settings.default_locale_name) if not request.accept_language: # If browser has no language configuration # the default locale name is returned. locale_name = request.registry.settings.default_locale_name return locale_name
12. Let Users Choose Their Language
As pointed out by Martijn Pieters is his StackOverflow answer [7], basing your web application language on the Accept-Language header can cause problems to users that do not know how to set their preferred languages in the browser. Therefore, to make sure that users can switch languages easily, we use a cookie to store that preference for future visits [9].
Add the set_locale_cookie() function to the i18n.py module:
from pyramid.view import view_config from pyramid.response import Response from pyramid.httpexceptions import HTTPFound #... #... @view_config(route_name='locale') def set_locale_cookie(request): if request.GET['language']: language = request.GET['language'] response = Response() response.set_cookie('_LOCALE_', value=language, max_age=31536000) # max_age = year return HTTPFound(location=request.environ['HTTP_REFERER'], headers=response.headers)
Add a new route in the configuration __init__py module:
#... config.add_route('locale', '/locale') #...
Then add your languages links to the template file:
<p> <a href="/locale?language=en">English</a> <a href="/locale?language=es">Español</a> <a href="/locale?language=it">Italiano</a> </p>
Conclusion
That's conclude this tutorial for now, we learned how to setup i18n, translate strings in our code and templates, determine the user language and let them choose their preferred one, sure there are more things to know about it, so I invite you to fork my pyramid_i18n_howto repository and send me your pull request to update it with new findings.
Danilo
This tutorial is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/.
The pyramid_i18n_howto source code is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
[1] | http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/project.html |
[2] | http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/i18n.html |
[3] | http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/templates/mako_i18n.html |
[4] | http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/i18n.html#using-a-localizer |
[5] | http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/hooks.html#adding-renderer-globals |
[6] | http://docs.pylonsproject.org/projects/pyramid/en/latest/api/config.html?highlight=add_subscriber#pyramid.config.Configurator.add_subscriber |
[7] | (1, 2) http://stackoverflow.com/questions/11274420/determine-the-user-language-in-pyramid |
[8] | http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/i18n.html#locale-negotiators |
[9] | http://stackoverflow.com/questions/8746087/pyramid-how-to-set-cookie-without-renderer |