Zope3 pagetemplates with i18n standalone
Shows how to use pagetemplates with most features in a python script without using Zope at all
Why
I love the concept of Zope Page Templates. They force you to separate logic from content and always generate valid xml or xhtml. It limits the danger of cross-site scripting vulnerabilities because everything that's dynamically inserted is escaped.
There are interesting implementations like simpletal or phptal which I used so far. Problem with phptal - it's for PHP ;). Other than that it's fully featured including internationalisation (i18n). And it is pretty fast, phptal generates php code from the templates, eliminating the parsing overhead. Problem with simpletal - no i18n and weird bugs when i18n is patched into it, like msgids within a macro aren't translated.
Why would I need to run it standalone whithout using Zope3? Well sometimes I have to quickly create scripts that churn out xhtml/xml, using a templating engine helps a lot. Also I like to use mod_python for smaller projects, having the cool templating engine without the overhead of Zope3 development sometimes makes sense.
The code:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
# python stuff
import gettext
# don't use cStringIO - it is unicode unaware!
from StringIO import StringIO
import os, re, datetime
# ZOPE 3 stuff
sys.path.insert(0,'/usr/lib/zope-3.3.0/lib/python/')
from zope.tal.talinterpreter import TALInterpreter
from zope.tales.tales import ExpressionEngine, Context
from zope.pagetemplate.pagetemplate import PageTemplate
# Documentation:
# http://wiki.zope.org/zope3/ZPTInternationalizationSupport
TranslationBasePath = '/path/to/translations'
# context object for translation
class TranslationContext(Context):
def __init__(self, language, engine, contexts):
Context.__init__(self, engine, contexts)
self.translation_file = os.path.join(TranslationBasePath, language + "/LC_MESSAGES/messages.mo")
def translate(self, msgid, domain=None, mapping=None, default=None):
#print msgid, domain, mapping, default
trans = gettext.GNUTranslations(open(self.translation_file))
translated = trans.ugettext(msgid)
def repl(m):
return unicode(mapping[m.group(m.lastindex).lower()])
cre = re.compile(r'\$(?:([_A-Za-z][-\w]*)|\{([_A-Za-z][-\w]*)\})')
return cre.sub(repl, translated)
class FSPageTemplateFolder:
def __init__(self, basepath):
self._basepath = basepath
def __getitem__(self, name):
fn = os.path.join(self._basepath, name)
if os.path.isdir(fn):
return FSPageTemplateFolder(fn)
txt = open(fn).read()
pt = PageTemplate()
pt.write(txt)
return pt
data = {
'test': 'Testing text',
'here': {'name': 'sepp', 'country_of_birth': 'lala', 'greeting': 'first name'},
'dt': datetime.datetime(2007, 5, 23),
'has_banana': True,
'container': FSPageTemplateFolder('/path/to/template')
}
t1 = u'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xlmns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="startpage"
i18n:source="en"
>
<h1 tal:content="test"></h1>
<p i18n:translate="">without limits</p>
<p tal:content="python: dt.today()"></p>
<span i18n:translate=''>
<span tal:replace='here/name' i18n:name='name' /> was born in
<span tal:replace='here/country_of_birth' i18n:name='country' />.
</span>
<div metal:use-macro="container/master.html/macros/thismonth">
<div metal:fill-slot="additional-notes">
<h1 tal:content="test"></h1>
</div>
</div>
<img src="http://foo.com/logo" alt="Visit us"
tal:attributes="alt here/greeting"
i18n:attributes="alt" />
<select>
<option tal:attributes="selected has_banana">banana</option>
</select>
<!-- <div i18n:data="dt" i18n:translate="msgid"></div> -->
</html>'''
context = TranslationContext('de', ExpressionEngine(),data)
_ = context.translate
print _('without limits')
buffer = StringIO()
pt = PageTemplate()
pt.pt_edit(t1, 'text/html')
pt._cook()
TALInterpreter(pt._v_program, pt._v_macros, context, buffer)()
macro = buffer.getvalue()
print "----"
print macro
master.html:
<html i18n:domain="CalendarService">
<!-- really hairy TAL code here ;-) -->
<p metal:define-macro="copyright">
Copyright 2001, <em>Foobar</em> Inc.
</p>
<div metal:define-macro="thismonth">
<p i18n:translate="">without limits</p>
<div metal:define-slot="additional-notes">
Place for the application to add additional notes if desired.
</div>
</div>
</html>
output:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xlmns="http://www.w3.org/1999/xhtml">
<h1>Testing text</h1>
<p>keine Einschränkung</p>
<p>2007-06-17 13:10:22.690563</p>
<span>sepp was born in lala.</span>
<div>
<p>keine Einschränkung</p>
<div>
<h1>Testing text</h1>
</div>
</div>
<img src="http://foo.com/logo" alt="Vorname" />
<select>
<option selected="selected">banana</option>
</select>
<!-- <div i18n:data="dt" i18n:translate="msgid"></div> -->
</html>
What happens here
- The stuff from zope3 gets imported. If you have a setup like me, multiple zope versions might be installed, they are not in the search_path, that's why i insert the path to my desired zope version at the beginning.
- A new class TranslationContext for my translation stuff, based on zope.tales.tales.Context - I used the easy approach with the gettext.GNUTranslations internally. More sophisticated solutions, with domains and whatnot are possible, but this is just a quick and dirty intro.
- A new class for the container object within the templates context. In Zope this is pointing somewhere inside the ZODB, we only have a filesystem, here's a quick implementation of a filesystem with templates FSPageTemplateFolder.
- data holds the information that goes into the template
- t1 is the template, it references to macros from master.html
- the transformation of the template, i use pt_edit instead of simply write because there I can specify if it is text/html or text/xml I want
What does not work
- i18n:data is somehow broken, I don't need it but it still would be interesting how to enable this feature.
- i18n:source and i18n:target don't seem to have any effect