Commit 0c1369b9 authored by Fred Drake's avatar Fred Drake

Back-port two features from the Zope 3 version of Page Templates:

- avoid normalizing whitespace when using the default text when there is not
  a matching translation
- added support for explicit msgids in the i18n:attributes syntax
parent 9bab3b8a
......@@ -13,14 +13,22 @@
##############################################################################
"""Global Translation Service for providing I18n to Page Templates.
$Id: GlobalTranslationService.py,v 1.3 2002/10/06 17:21:07 efge Exp $
$Id: GlobalTranslationService.py,v 1.4 2003/04/07 17:38:27 fdrake Exp $
"""
import re
from DocumentTemplate.DT_Util import ustr
from TAL.TALDefs import NAME_RE
class DummyTranslationService:
"""Translation service that doesn't know anything about translation."""
def translate(self, domain, msgid, mapping=None,
context=None, target_language=None):
return None
context=None, target_language=None, default=None):
def repl(m, mapping=mapping):
return ustr(mapping[m.group(m.lastindex)])
cre = re.compile(r'\$(?:(%s)|\{(%s)\})' % (NAME_RE, NAME_RE))
return cre.sub(repl, default or msgid)
# XXX Not all of Zope.I18n.ITranslationService is implemented.
translationService = DummyTranslationService()
......
......@@ -15,7 +15,7 @@
An implementation of a generic TALES engine
"""
__version__='$Revision: 1.36 $'[11:-2]
__version__='$Revision: 1.37 $'[11:-2]
import re, sys, ZTUtils
from MultiMapping import MultiMapping
......@@ -247,12 +247,14 @@ class Context:
self.position = position
def translate(self, domain, msgid, mapping=None,
context=None, target_language=None):
context=None, target_language=None, default=None):
if context is None:
context = self.contexts.get('here')
return getGlobalTranslationService().translate(
domain, msgid, mapping=mapping,
context=context, target_language=target_language)
context=context,
default=default,
target_language=target_language)
class TALESTracebackSupplement:
"""Implementation of ITracebackSupplement"""
......
......@@ -206,12 +206,18 @@ class DummyEngine:
def getDefault(self):
return Default
def translate(self, domain, msgid, mapping):
return self.translationService.translate(domain, msgid, mapping)
def translate(self, domain, msgid, mapping, default=None):
return self.translationService.translate(domain, msgid, mapping,
default=default)
class Iterator:
# This is not an implementation of a Python iterator. The next()
# method returns true or false to indicate whether another item is
# available; if there is another item, the iterator instance calls
# setLocal() on the evaluation engine passed to the constructor.
def __init__(self, name, seq, engine):
self.name = name
self.seq = seq
......@@ -232,26 +238,33 @@ class DummyDomain:
__implements__ = IDomain
def translate(self, msgid, mapping=None, context=None,
target_language=None):
target_language=None, default=None):
# This is a fake translation service which simply uppercases non
# ${name} placeholder text in the message id.
#
# First, transform a string with ${name} placeholders into a list of
# substrings. Then upcase everything but the placeholders, then glue
# things back together.
# simulate an unknown msgid by returning None
if msgid == "don't translate me":
text = default
else:
text = msgid.upper()
def repl(m, mapping=mapping):
return ustr(mapping[m.group(m.lastindex).lower()])
cre = re.compile(r'\$(?:([_A-Z]\w*)|\{([_A-Z]\w*)\})')
return cre.sub(repl, msgid.upper())
cre = re.compile(r'\$(?:(%s)|\{(%s)\})' % (NAME_RE, NAME_RE))
return cre.sub(repl, text)
class DummyTranslationService:
__implements__ = ITranslationService
def translate(self, domain, msgid, mapping=None, context=None,
target_language=None):
# Ignore domain
target_language=None, default=None):
return self.getDomain(domain).translate(msgid, mapping, context,
target_language)
target_language,
default=default)
def getDomain(self, domain):
return DummyDomain()
......@@ -28,7 +28,9 @@ ZOPE_TAL_NS = "http://xml.zope.org/namespaces/tal"
ZOPE_METAL_NS = "http://xml.zope.org/namespaces/metal"
ZOPE_I18N_NS = "http://xml.zope.org/namespaces/i18n"
NAME_RE = "[a-zA-Z_][a-zA-Z0-9_]*"
# This RE must exactly match the expression of the same name in the
# zope.i18n.simpletranslationservice module:
NAME_RE = "[a-zA-Z_][-a-zA-Z0-9_]*"
KNOWN_METAL_ATTRIBUTES = [
"define-macro",
......@@ -118,12 +120,12 @@ def parseAttributeReplacements(arg, xml):
for part in splitParts(arg):
m = _attr_re.match(part)
if not m:
raise TALError("Bad syntax in attributes:" + `part`)
raise TALError("Bad syntax in attributes: " + `part`)
name, expr = m.group(1, 2)
if not xml:
name = name.lower()
if dict.has_key(name):
raise TALError("Duplicate attribute name in attributes:" + `part`)
raise TALError("Duplicate attribute name in attributes: " + `part`)
dict[name] = expr
return dict
......
......@@ -29,6 +29,9 @@ I18N_REPLACE = 1
I18N_CONTENT = 2
I18N_EXPRESSION = 3
_name_rx = re.compile(NAME_RE)
class TALGenerator:
inMacroUse = 0
......@@ -329,6 +332,9 @@ class TALGenerator:
# calculate the contents of the variable, e.g.
# "I live in <span i18n:name="country"
# tal:replace="here/countryOfOrigin" />"
m = _name_rx.match(varname)
if m is None or m.group() != varname:
raise TALError("illegal i18n:name: %r" % varname, self.position)
key = cexpr = None
program = self.popProgram()
if action == I18N_REPLACE:
......@@ -458,13 +464,13 @@ class TALGenerator:
for item in attrlist:
key = item[0]
if repldict.has_key(key):
expr, xlat = repldict[key]
item = item[:2] + ("replace", expr, xlat)
expr, xlat, msgid = repldict[key]
item = item[:2] + ("replace", expr, xlat, msgid)
del repldict[key]
newlist.append(item)
# Add dynamic-only attributes
for key, (expr, xlat) in repldict.items():
newlist.append((key, None, "insert", expr, xlat))
for key, (expr, xlat, msgid) in repldict.items():
newlist.append((key, None, "insert", expr, xlat, msgid))
return newlist
def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
......@@ -651,16 +657,18 @@ class TALGenerator:
else:
repldict = {}
if i18nattrs:
i18nattrs = i18nattrs.split()
i18nattrs = _parseI18nAttributes(i18nattrs, self.position,
self.xml)
else:
i18nattrs = ()
i18nattrs = {}
# Convert repldict's name-->expr mapping to a
# name-->(compiled_expr, translate) mapping
for key, value in repldict.items():
repldict[key] = self.compileExpression(value), key in i18nattrs
ce = self.compileExpression(value)
repldict[key] = ce, key in i18nattrs, i18nattrs.get(key)
for key in i18nattrs:
if not repldict.has_key(key):
repldict[key] = None, 1
repldict[key] = None, 1, i18nattrs.get(key)
else:
repldict = {}
if replace:
......@@ -782,6 +790,30 @@ class TALGenerator:
if defineMacro:
self.emitDefineMacro(defineMacro)
def _parseI18nAttributes(i18nattrs, position, xml):
d = {}
for spec in i18nattrs.split(";"):
parts = spec.split()
if len(parts) > 2:
raise TALError("illegal i18n:attributes specification: %r" % spec,
position)
if len(parts) == 2:
attr, msgid = parts
else:
# len(parts) == 1
attr = parts[0]
msgid = None
if not xml:
attr = attr.lower()
if attr in d:
raise TALError(
"attribute may only be specified once in i18n:attributes: %r"
% attr,
position)
d[attr] = msgid
return d
def test():
t = TALGenerator()
t.pushProgram()
......
......@@ -349,7 +349,7 @@ class TALInterpreter:
return self.attrAction(item)
name, value, action = item[:3]
ok = 1
expr, msgid = item[3:]
expr, xlat, msgid = item[3:]
if self.html and name.lower() in BOOLEAN_HTML_ATTRS:
evalue = self.engine.evaluateBoolean(item[3])
if evalue is self.Default:
......@@ -368,23 +368,18 @@ class TALInterpreter:
if evalue is None:
ok = 0
value = evalue
if msgid:
value = self.i18n_attribute(value)
if value is None:
value = name
value = '%s="%s"' % (name, escape(value, 1))
if ok:
if xlat:
translated = self.translate(msgid or value, value, {})
if translated is not None:
value = translated
if value is None:
value = name
value = '%s="%s"' % (name, escape(value, 1))
return ok, name, value
bytecode_handlers["<attrAction>"] = attrAction
def i18n_attribute(self, s):
# s is the value of an attribute before translation
# it may have been computed
xlated = self.translate(s, {})
if xlated is None:
return s
else:
return xlated
def no_tag(self, start, program):
state = self.saveState()
self.stream = stream = self.StringIO()
......@@ -553,36 +548,19 @@ class TALInterpreter:
# subnodes, which should /not/ go to the output stream.
tmpstream = self.StringIO()
self.interpretWithStream(stuff[1], tmpstream)
content = None
default = tmpstream.getvalue()
# We only care about the evaluated contents if we need an implicit
# message id. All other useful information will be in the i18ndict on
# the top of the i18nStack.
if msgid == '':
content = tmpstream.getvalue()
msgid = normalize(content)
msgid = normalize(default)
self.i18nStack.pop()
# See if there is was an i18n:data for msgid
if len(stuff) > 2:
obj = self.engine.evaluate(stuff[2])
xlated_msgid = self.translate(msgid, i18ndict, obj)
# If there is no translation available, use evaluated content.
if xlated_msgid is None:
if content is None:
content = tmpstream.getvalue()
# We must do potential substitutions "by hand".
s = interpolate(content, i18ndict)
else:
# XXX I can't decide whether we want to cgi escape the translated
# string or not. OT1H not doing this could introduce a cross-site
# scripting vector by allowing translators to sneak JavaScript into
# translations. OTOH, for implicit interpolation values, we don't
# want to escape stuff like ${name} <= "<b>Timmy</b>".
#s = escape(xlated_msgid)
s = xlated_msgid
# If there are i18n variables to interpolate into this string, better
# do it now.
# XXX efge: actually, this is already done by the translation service.
self._stream_write(s)
xlated_msgid = self.translate(msgid, default, i18ndict, obj)
assert xlated_msgid is not None, self.position
self._stream_write(xlated_msgid)
bytecode_handlers['insertTranslation'] = do_insertTranslation
def do_insertStructure(self, stuff):
......@@ -636,23 +614,14 @@ class TALInterpreter:
self.interpret(block)
bytecode_handlers["loop"] = do_loop
def translate(self, msgid, i18ndict=None, obj=None):
# XXX is this right?
if i18ndict is None:
i18ndict = {}
def translate(self, msgid, default, i18ndict, obj=None):
if obj:
i18ndict.update(obj)
# XXX need to fill this in with TranslationService calls. For now,
# we'll just do simple interpolation based on a $-strings to %-strings
# algorithm in Mailman.
if not self.i18nInterpolate:
return msgid
# XXX Mmmh, it seems that sometimes the msgid is None; is that really
# possible?
if msgid is None:
return None
# XXX We need to pass in one of context or target_language
return self.engine.translate(self.i18nContext.domain, msgid, i18ndict)
return self.engine.translate(self.i18nContext.domain,
msgid, i18ndict, default=default)
def do_rawtextColumn(self, (s, col)):
self._stream_write(s)
......
......@@ -53,7 +53,7 @@ FILE = "tests/input/test01.xml"
class TestTranslations(DummyTranslationService):
def translate(self, domain, msgid, mapping=None, context=None,
target_language=None):
target_language=None, default=None):
if msgid == 'timefmt':
return '%(minutes)s minutes after %(hours)s %(ampm)s' % mapping
elif msgid == 'jobnum':
......@@ -67,7 +67,8 @@ class TestTranslations(DummyTranslationService):
return '%(name)s was born in %(country)s' % mapping
return DummyTranslationService.translate(self, domain, msgid,
mapping, context,
target_language)
target_language,
default=default)
class TestEngine(DummyEngine):
def __init__(self, macros=None):
......
<span i18n:translate="don't translate me">
stuff
<span tal:replace="string:foobar" i18n:name="longname" />
more stuff
</span>
<span i18n:translate="">
stuff
<span tal:replace="string:foobar" i18n:name="longname" />
more stuff
</span>
<input value="don't translate me"
i18n:attributes="value">
<span>
stuff
foobar
more stuff
</span>
<span>STUFF foobar MORE STUFF</span>
......@@ -400,11 +400,11 @@ class TALGeneratorTestCases(TestCaseBase):
{'tal:attributes': 'href string:http://www.zope.org; x string:y',
'name': 'bar', 'href': 'foo'}),
('startTag', ('a',
[('href', 'foo', 'replace', '$string:http://www.zope.org$', 0),
[('href', 'foo', 'replace', '$string:http://www.zope.org$', 0, None),
('name', 'name="bar"'),
('tal:attributes',
'href string:http://www.zope.org; x string:y', 'tal'),
('x', None, 'insert', '$string:y$', 0)])),
('x', None, 'insert', '$string:y$', 0, None)])),
('endScope', ()),
rawtext('link</a>'),
])
......@@ -418,7 +418,7 @@ class TALGeneratorTestCases(TestCaseBase):
'tal:replace': 'structure string:<img>'}),
('insertStructure',
('$string:<img>$',
{'src': ('$string:foo.png$', 0)},
{'src': ('$string:foo.png$', 0, None)},
[('startTag', ('p',
[('tal:replace', 'structure string:<img>', 'tal'),
('tal:attributes', 'src string:foo.png',
......@@ -496,10 +496,27 @@ class TALGeneratorTestCases(TestCaseBase):
('setPosition', (1, 0)),
('beginScope', {'alt': 'foo', 'i18n:attributes': 'alt'}),
('startTag', ('img',
[('alt', 'foo', 'replace', None, 1),
[('alt', 'foo', 'replace', None, 1, None),
('i18n:attributes', 'alt', 'i18n')])),
('endScope', ()),
])
self._run_check("<img alt='foo' i18n:attributes='alt foo ; bar'>", [
('setPosition', (1, 0)),
('beginScope', {'alt': 'foo', 'i18n:attributes': 'alt foo ; bar'}),
('startTag', ('img',
[('alt', 'foo', 'replace', None, 1, 'foo'),
('i18n:attributes', 'alt foo ; bar', 'i18n'),
('bar', None, 'insert', None, 1, None)])),
('endScope', ()),
])
def test_i18n_name_bad_name(self):
self._should_error("<span i18n:name='not a valid name' />")
self._should_error("<span i18n:name='-bad-name' />")
def test_i18n_attributes_repeated_attr(self):
self._should_error("<a i18n:attributes='href; href' />")
self._should_error("<a i18n:attributes='href; HREF' />")
def check_i18n_translate(self):
# input/test19.html
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment