Project Internationalization with Nagare (message extraction)
Goals
Nagare doesn't have tight integration with gettext or Babel, here we're going to see a way to use those tools in a nagare project.
In the first part, we will integrate Babel into the project, in order to manage catalogs and extract messages. In the second part, we will set up effective message translation inside the project.
Requirements
We need a working Nagare Installation.
We will use Babel to extract the template file and manage our catalog files, so let's install it:
$easy_install Babel
...
Then, we will create and register the example application this way
$ nagare-admin create-app example_i18n
Application 'example_i18n' created.
...
$ cd example_i18n
$ python setup.py develop
...
Finished processing dependencies for example-i18n==0.0.1
Set some defaults for Babel commands
Create a setup.cfg in your newly created app folder containing the following
[extract_messages]
keywords = _ , ugettext , lazy_ugettext:1 , N_:1,2 , ungettext:1,2 , lazy_ungettext:1,2
output_file = data/locale/messages.pot
[init_catalog]
input_file = data/locale/messages.pot
output_dir = data/locale
domain = messages
[update_catalog]
input_file = data/locale/messages.pot
output_dir = data/locale
domain = messages
[compile_catalog]
directory = data/locale
domain = messages
Here, we decide to put translation files into data/locale, but this folder is not created by create-app so we have to create it manually:
$ mkdir data/locale
And finally, we add message extractors information into out setup.py file:
VERSION = '0.0.1'
from setuptools import setup, find_packages
setup(
name = 'example_i18n',
version = VERSION,
author = '',
author_email = '',
description = '',
license = '',
keywords = '',
url = '',
packages = find_packages(),
include_package_data = True,
package_data = {'' : ['*.cfg']},
zip_safe = False,
install_requires = ('nagare', 'Babel'),
entry_points = """
[nagare.applications]
example_i18n = example_i18n.app:app
""",
message_extractors = {'example_i18n': [
('**.py', 'python', None)]}
)
- Note:
- Babel has been added as a dependency for the project, this way typing 'python setup.py develop' should install Babel if it is missing.
Prepare application for internationalization
Edit example_i18n/app.py, put every strings between _() and define a fake _ function:
from __future__ import with_statement
import os
from nagare import presentation
_ = lambda x: x
class Example_i18n(object):
pass
@presentation.render_for(Example_i18n)
def render(self, h, *args):
this_file = __file__
if this_file.endswith('.pyc'):
this_file = __file__[:-1]
models_file = os.path.join(os.path.dirname(__file__), 'models.py')
h.head.css_url('/static/nagare/application.css')
h.head << h.head.title(_('Up and Running !'))
with h.div(class_='mybody'):
with h.div(id='myheader'):
h << h.a(h.img(src='/static/nagare/img/logo.gif'), id='logo', href='http://www.nagare.org/', title='Nagare home')
h << h.span(_('Congratulations !'), id='title')
with h.div(id='main'):
h << h.h1(_('Your application is running'))
with h.p:
h << _('You can now:')
with h.ul:
h << h.li(_('If your application uses a database, add your database entities into '), h.i(models_file))
h << h.li(_('Add your application components into '), h.i(this_file), _(' or create new files'))
h << h.p(_('To learn more, go to the '), h.a(_('official website'), href='http://www.nagare.org/'))
h << _("Have fun !")
h << h.div(class_='footer')
return h.root
# ---------------------------------------------------------------
app = Example_i18n
Message extraction and catalog manipulation
Messages can be extracted:
$ python setup.py extract_messages
running extract_messages
extracting messages from example_i18n/__init__.py
extracting messages from example_i18n/app.py
extracting messages from example_i18n/models.py
writing PO template file to data/locale/messages.pot
A messages.pot file is now created into data/locale
For the next step, we have to initialize a catalog for your preferred locale (fr in this example):
$ python setup.py init_catalog -l fr
running init_catalog
creating catalog 'data/locale/fr/LC_MESSAGES/messages.po' based on 'data/locale/messages.pot'
The catalog is now added and is ready to be edited.
Once edited and filled in with the right translations, we can compile our catalog:
$ python setup.py compile_catalog
running compile_catalog
compiling catalog 'data/locale/fr/LC_MESSAGES/messages.po' to 'data/locale/fr/LC_MESSAGES/messages.mo'
This way we can prepare as many catalogs as locales needed.
- Note:
- when changing messages in a project we have to do a extract_messages followed by a update_catalog, and add missing translation in every modified catalog.
Next Step
We are using a fake _ function, so nothing is translated at all, the next step is to plug gettext functions into example_i18n application, in order to get effective message translation.
A simple tabbed navigation with Nagare
Tabs are a widely used mean to navigate between categories or sections of a website. There are lots of howtos and tutorials to create clean tab based navigation with HTML and CSS.
Let's see how to handle such a navigation with Nagare. We are going to create a tabbed navigation between a TicTacToe and a Counter.
Requirements
We use TicTacToe and Counter examples from our Demonstration Portal. Hence nagare examples have to be installed with following command:
easy_install nagare.examples
To run our small example we use serve-module:
nagare-admin serve-module ./menu_example.py:TabNav menu_example
Some styling
Finally a bit of CSS can be added to get the tabbed rendering effect.
@presentation.render_for(TabNav)
def render(self, h, *args):
css = """
body {
font-family: verdana,'Bitstream Vera Sans',helvetica,arial,sans-serif;
}
ul.menu {
height: 2em;
list-style: none;
margin: 0;
padding: 0;
}
ul.menu li {
float: left;
margin: 0 1px 0 0;
padding-left: 10px;
border: 1px solid #555555;
border-bottom: none;
color: #555555;
}
ul.menu a {
color: #555555;
display: block;
float: left;
height: 2em;
line-height: 2em;
padding-right: 10px;
text-decoration: none;
}
ul.menu li.selected-entry {
background-color: #555555;
}
ul.menu li.selected-entry a {
color: #FFFFFF;
font-weight: bold;
}
div.content {
border: #555555 solid 2px;
clear: left;
padding: 1em;
}
"""
h << h.style(css, type="text/css")
h << self.menu
with h.div(class_='content'):
h << self.content
return h.root
This renders as following:
- Rq:
- As we use serve-module, CSS has to be embedded into HTML but it is usually put in the static folder of the application skeleton.
Use Nagare namespaces to create an rss feed
In Nagare HTML and XML generation is done through Renderers. Thus, to generate RSS feeds it is possible to use Nagare XML Renderer, which accept any tag or attribute. Another way is to create a RSS Renderer for Nagare which matches RSS namespace.
To do so we are extending Nagare XmlRender, which enforces possible xml tags and attributes (e.g. namespace).
Simple RSSRenderer
RSS specification, leads to the following namespace definition in Nagare:
class RssRenderer(xml.XmlRenderer):
"""RSS renderer"""
# RSS tags
# ------------
rss = TagProp('rss', set(('version', )))
channel = TagProp('channel', set())
title = TagProp('title', set())
link = TagProp('link', set())
description = TagProp('description', set())
language = TagProp('language', set())
copyright = TagProp('copyright', set())
managingEditor = TagProp('managingEditor', set())
webMaster = TagProp('webMaster', set())
pubDate = TagProp('pubDate', set())
lastBuildDate = TagProp('lastBuildDate', set())
category = TagProp('category', set())
generator = TagProp('generator', set())
docs = TagProp('docs', set())
cloud = TagProp('cloud', set(('domain', 'port',
'path', 'registerProcedure', 'protocol')))
ttl = TagProp('ttl', set())
image = TagProp('image', set())
url = TagProp('url', set())
width = TagProp('width', set())
height = TagProp('height', set())
rating = TagProp('rating', set())
textInput = TagProp('textInput', set())
name = TagProp('name', set())
skipHours = TagProp('skipHours', set())
skipDays = TagProp('skipDays', set())
item = TagProp('item', set())
title = TagProp('title', set())
link = TagProp('link', set())
description = TagProp('description', set())
author = TagProp('author', set())
category = TagProp('category', set(('domain', )))
comments = TagProp('comments', set())
enclosure = TagProp('enclosure', set(('url', 'length', 'type')))
guid = TagProp('guid', set(('isPermaLink', )))
pubDate = TagProp('pubDate', set())
source = TagProp('source', set(('url', )))
Note
Notes:
- RssRenderer checks if tags and attibutes exist in namespace
- RssRenderer does not validate against any DTD or schema
Let's look at the following example:
<?xml version="1.0"?>
<rss version="2.0">
<channel>
<title>Some Blog Title</title>
<link>http://someblog.example.com</link>
<description>Description</description>
<language>en-us</language>
<pubDate>Tue, 25 Jan 2010 14:00:00 GMT</pubDate>
<lastBuildDate>Tue, 25 Jan 2010 14:00:00 GMT</lastBuildDate>
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
<generator>Some Generator</generator>
<managingEditor>editor@example.com</managingEditor>
<webMaster>webmaster@example.com</webMaster>
<item>
<title>Some Post Title</title>
<link>http://someblog.example.com/post1</link>
<description>Some Post Description</description>
<pubDate>Tue, 25 Jan 2010 14:00:00 GMT</pubDate>
<guid>Some Global Unique Identifier</guid>
</item>
</channel>
</rss>
With RssRenderer, this feed is generated this way:
r = rss.RssRenderer()
with r.rss(version="2.0"):
with r.channel:
r << r.title('Some Blog Title')
r << r.link('http://someblog.example.com')
r << r.description('Description')
r << r.language('en-us')
r << r.pubDate('Tue, 25 Jan 2010 14:00:00 GMT')
r << r.lastBuildDate('Tue, 25 Jan 2010 14:00:00 GMT')
r << r.docs('http://blogs.law.harvard.edu/tech/rss')
r << r.generator('Some Generator')
r << r.managingEditor('editor@example.com')
r << r.webMaster('webmaster@example.com')
with r.item:
r << r.title('Some Post Title')
r << r.link('http://someblog.example.com/post1')
r << r.description('Some Post Description')
r << r.pubDate('Tue, 25 Jan 2010 14:00:00 GMT')
r << r.guid('Some Global Unique Identifier')
Now RSSRenderer can be used to generate any RSS feed.
Mixing multiple namespaces
There's several extensions to RSS, actually extensions are just XML namespaces added in the feed. One simple extension is blogChannel namespace.
First create a BlogChannelRenderer:
class BlogChannelRenderer(xml.XmlRenderer):
blogRoll = TagProp('blogRoll', set())
mySubscriptions = TagProp('mySubscriptions', set())
blink = TagProp('blink', set())
changes = TagProp('changes', set())
Now we can mix RssRenderer and BlogChannelRenderer to create one feed:
r = rss.RssRenderer()
r.namespaces = {'blogChannel': 'http://backend.userland.com/blogChannelModule'}
b = rss.BlogChannelRenderer(r)
b.default_namespace = 'blogChannel'
with r.rss(version="2.0"):
with r.channel:
r << r.title('Some Blog Title')
r << r.link('http://someblog.example.com')
r << r.description('Description')
r << r.language('en-us')
r << b.blogRoll('http://someblog.example.com/blogroll')
r << b.mySubscriptions('http://someblog.example.com/subscriptions')
r << b.blink('http://someblog.example.com/backlink')
r << b.changes('http://someblog.example.com/changes')
r << r.pubDate('Tue, 25 Jan 2010 14:00:00 GMT')
r << r.lastBuildDate('Tue, 25 Jan 2010 14:00:00 GMT')
r << r.docs('http://blogs.law.harvard.edu/tech/rss')
r << r.generator('Some Generator')
r << r.managingEditor('editor@example.com')
r << r.webMaster('webmaster@example.com')
with r.item:
r << r.title('Some Post Title')
r << r.link('http://someblog.example.com/post1')
r << r.description('Some Post Description')
r << r.pubDate('Tue, 25 Jan 2010 14:00:00 GMT')
r << r.guid('Some Global Unique Identifier')
This produces the following RSS feed:
<rss xmlns:blogChannel="http://backend.userland.com/blogChannelModule" version="2.0">
<channel>
<title>Some Blog Title</title>
<link>http://someblog.example.com</link>
<description>Description</description>
<language>en-us</language>
<blogChannel:blogRoll>http://someblog.example.com/blogroll</blogChannel:blogRoll>
<blogChannel:mySubscriptions>http://someblog.example.com/subscriptions</blogChannel:mySubscriptions>
<blogChannel:blink>http://someblog.example.com/backlink</blogChannel:blink>
<blogChannel:changes>http://someblog.example.com/changes</blogChannel:changes>
<pubDate>Tue, 25 Jan 2010 14:00:00 GMT</pubDate>
<lastBuildDate>Tue, 25 Jan 2010 14:00:00 GMT</lastBuildDate>
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
<generator>Some Generator</generator>
<managingEditor>editor@example.com</managingEditor>
<webMaster>webmaster@example.com</webMaster>
<item>
<title>Some Post Title</title>
<link>http://someblog.example.com/post1</link>
<description>Some Post Description</description>
<pubDate>Tue, 25 Jan 2010 14:00:00 GMT</pubDate>
<guid>Some Global Unique Identifier</guid>
</item>
</channel>
</rss>
With this process any XML namespace can be added to Nagare. For example, Nagare XHTML namespace is implemented as a Renderer.
Nagare installation on Mac OS X (Snow Leopard)
Snow Leopard has been released this summer.
The 64-bits Mac OS X version is installed by default on new Mac. Currently, the use of Nagare with the 64-bits stackless Mac OS X version leads to a Bus Error .
To get around this, we need to install a 32-bits stackless python, thus changing slightly the install procedure for Mac OS X described previously .
Install MacPorts
Install instructions here.
Configure MacPorts
MacPorts has to use universal variant (64 + 32 bits compilation).
In order to do so, you add +universal into variants.conf file, i.e. for default macports installation:
echo +universal | sudo tee -a /opt/local/etc/macports/variants.conf
Install Stackless
Now we have to run configure for 32-bits architecture:
cd /tmp
wget http://www.stackless.com/binaries/stackless-264-export.tar.bz2
tar jxvf stackless-264-export.tar.bz2
cd stackless-2.6.4*
CC="gcc -arch i386" ./configure --enable-stacklessfewerregisters --prefix=<STACKLESS_HOME>
make all
sudo make install
Once Stackless Python installed, Nagare installation goes on as usual.
Nagare installation on Mac OS X (Leopard)
As Mac OS X is a unix based system, the Linux section of NagareInstallation#linux-installation applies, but a few tweaks are needed to setup a fully working nagare environment.
We'll need the following external tools:
Install Xcode
Xcode should be available from your Applications Install DVD or can be downloaded from here.
Install MacPorts
Install instructions can be found here.
Install Stackless
Here we have to give some specific options to configure:
cd /tmp
wget http://www.stackless.com/binaries/stackless-252-export.tar.bz2
tar jxvf stackless-252-export.tar.bz2
cd stackless-2.5.2*
./configure --enable-stacklessfewerregisters --enable-universalsdk --prefix=<STACKLESS_HOME>
make all
sudo make install
--enable-stacklessfewerregisters and --enable-universalsdk are the key options here. Also note how Stackless Python is installed in its own <STACKLESS_HOME> directory, in order not to override the Python system.
Install libxml2 and libxslt
As the libxml2 version given with Xcode prevents lxml from installing, we'll use MacPorts to install the right one:
sudo port install libxslt
Finish nagare installation
From now on, you can follow the vanilla install guide, starting with section 2.
rss