Posts by author crumble

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

Create Menu component

The Menu has to meet the following needs:

  1. store the selected entry
  2. display the list of clickable entries
  3. notify when an entry is clicked

A fairly simple implementation in Nagare could be:

from nagare import presentation, var


class Menu:
    def __init__(self, menu):
        self.menu = menu
        self.selected = var.Var(0)

    def display(self, i, comp):
        comp.answer(i)


@presentation.render_for(Menu)
def render(self, h, comp, *args):
    with h.ul(class_='menu'):
        for index, entry in enumerate(self.menu):
            h << h.li(h.a(entry).action(lambda index=index: self.display(index, comp)),
                      class_='selected-entry' if self.selected() == index else '')
    return h.root

Notification of the clicked entry is done through the use of answer() method of the Menu component.

Put it together in TabNav component

TabNav is composed of two parts, menu and content. TabNav is expected to work the following way:

  1. When an entry is clicked in menu, select_tab is called
  2. select_tab change the displayed content and then sets the selected entry in menu
from nagare.examples import counter, tictactoe

from nagare import presentation, component

class TabNav:

    tab_factories = [('Counter', counter.Counter1),
                     ('TicTacToe', tictactoe.Task)]

    def __init__(self):
        self.menu = component.Component(Menu([label for label, factory in self.tab_factories]))
        self.menu.on_answer(self.select_tab)

        self.content = component.Component(None)

        self.select_tab(0)

    def select_tab(self, index):
        self.content.becomes(self.tab_factories[index][1]())
        self.menu().selected(index)


@presentation.render_for(TabNav)
def render(self, h, *args):

    h << self.menu
    with h.div(class_='content'):
        h << self.content

    return h.root

on_answer() method of the Menu component let us bind answer() method of the Menu component with select_tab() method of TabNav component. This way select_tab is called with the index of the clicked entry in the menu as parameter.

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:

/trac/raw-attachment/blog/tabbed-navigation-with-nagare/tabbed_navigation.2.png
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.