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 @@ ...@@ -13,14 +13,22 @@
############################################################################## ##############################################################################
"""Global Translation Service for providing I18n to Page Templates. """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: class DummyTranslationService:
"""Translation service that doesn't know anything about translation.""" """Translation service that doesn't know anything about translation."""
def translate(self, domain, msgid, mapping=None, def translate(self, domain, msgid, mapping=None,
context=None, target_language=None): context=None, target_language=None, default=None):
return 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. # XXX Not all of Zope.I18n.ITranslationService is implemented.
translationService = DummyTranslationService() translationService = DummyTranslationService()
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
An implementation of a generic TALES engine An implementation of a generic TALES engine
""" """
__version__='$Revision: 1.36 $'[11:-2] __version__='$Revision: 1.37 $'[11:-2]
import re, sys, ZTUtils import re, sys, ZTUtils
from MultiMapping import MultiMapping from MultiMapping import MultiMapping
...@@ -247,12 +247,14 @@ class Context: ...@@ -247,12 +247,14 @@ class Context:
self.position = position self.position = position
def translate(self, domain, msgid, mapping=None, def translate(self, domain, msgid, mapping=None,
context=None, target_language=None): context=None, target_language=None, default=None):
if context is None: if context is None:
context = self.contexts.get('here') context = self.contexts.get('here')
return getGlobalTranslationService().translate( return getGlobalTranslationService().translate(
domain, msgid, mapping=mapping, domain, msgid, mapping=mapping,
context=context, target_language=target_language) context=context,
default=default,
target_language=target_language)
class TALESTracebackSupplement: class TALESTracebackSupplement:
"""Implementation of ITracebackSupplement""" """Implementation of ITracebackSupplement"""
......
...@@ -206,12 +206,18 @@ class DummyEngine: ...@@ -206,12 +206,18 @@ class DummyEngine:
def getDefault(self): def getDefault(self):
return Default return Default
def translate(self, domain, msgid, mapping): def translate(self, domain, msgid, mapping, default=None):
return self.translationService.translate(domain, msgid, mapping) return self.translationService.translate(domain, msgid, mapping,
default=default)
class Iterator: 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): def __init__(self, name, seq, engine):
self.name = name self.name = name
self.seq = seq self.seq = seq
...@@ -232,26 +238,33 @@ class DummyDomain: ...@@ -232,26 +238,33 @@ class DummyDomain:
__implements__ = IDomain __implements__ = IDomain
def translate(self, msgid, mapping=None, context=None, 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 # This is a fake translation service which simply uppercases non
# ${name} placeholder text in the message id. # ${name} placeholder text in the message id.
# #
# First, transform a string with ${name} placeholders into a list of # First, transform a string with ${name} placeholders into a list of
# substrings. Then upcase everything but the placeholders, then glue # substrings. Then upcase everything but the placeholders, then glue
# things back together. # 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): def repl(m, mapping=mapping):
return ustr(mapping[m.group(m.lastindex).lower()]) return ustr(mapping[m.group(m.lastindex).lower()])
cre = re.compile(r'\$(?:([_A-Z]\w*)|\{([_A-Z]\w*)\})') cre = re.compile(r'\$(?:(%s)|\{(%s)\})' % (NAME_RE, NAME_RE))
return cre.sub(repl, msgid.upper()) return cre.sub(repl, text)
class DummyTranslationService: class DummyTranslationService:
__implements__ = ITranslationService __implements__ = ITranslationService
def translate(self, domain, msgid, mapping=None, context=None, def translate(self, domain, msgid, mapping=None, context=None,
target_language=None): target_language=None, default=None):
# Ignore domain
return self.getDomain(domain).translate(msgid, mapping, context, return self.getDomain(domain).translate(msgid, mapping, context,
target_language) target_language,
default=default)
def getDomain(self, domain): def getDomain(self, domain):
return DummyDomain() return DummyDomain()
...@@ -28,7 +28,9 @@ ZOPE_TAL_NS = "http://xml.zope.org/namespaces/tal" ...@@ -28,7 +28,9 @@ ZOPE_TAL_NS = "http://xml.zope.org/namespaces/tal"
ZOPE_METAL_NS = "http://xml.zope.org/namespaces/metal" ZOPE_METAL_NS = "http://xml.zope.org/namespaces/metal"
ZOPE_I18N_NS = "http://xml.zope.org/namespaces/i18n" 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 = [ KNOWN_METAL_ATTRIBUTES = [
"define-macro", "define-macro",
...@@ -118,12 +120,12 @@ def parseAttributeReplacements(arg, xml): ...@@ -118,12 +120,12 @@ def parseAttributeReplacements(arg, xml):
for part in splitParts(arg): for part in splitParts(arg):
m = _attr_re.match(part) m = _attr_re.match(part)
if not m: if not m:
raise TALError("Bad syntax in attributes:" + `part`) raise TALError("Bad syntax in attributes: " + `part`)
name, expr = m.group(1, 2) name, expr = m.group(1, 2)
if not xml: if not xml:
name = name.lower() name = name.lower()
if dict.has_key(name): if dict.has_key(name):
raise TALError("Duplicate attribute name in attributes:" + `part`) raise TALError("Duplicate attribute name in attributes: " + `part`)
dict[name] = expr dict[name] = expr
return dict return dict
......
...@@ -29,6 +29,9 @@ I18N_REPLACE = 1 ...@@ -29,6 +29,9 @@ I18N_REPLACE = 1
I18N_CONTENT = 2 I18N_CONTENT = 2
I18N_EXPRESSION = 3 I18N_EXPRESSION = 3
_name_rx = re.compile(NAME_RE)
class TALGenerator: class TALGenerator:
inMacroUse = 0 inMacroUse = 0
...@@ -329,6 +332,9 @@ class TALGenerator: ...@@ -329,6 +332,9 @@ class TALGenerator:
# calculate the contents of the variable, e.g. # calculate the contents of the variable, e.g.
# "I live in <span i18n:name="country" # "I live in <span i18n:name="country"
# tal:replace="here/countryOfOrigin" />" # 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 key = cexpr = None
program = self.popProgram() program = self.popProgram()
if action == I18N_REPLACE: if action == I18N_REPLACE:
...@@ -458,13 +464,13 @@ class TALGenerator: ...@@ -458,13 +464,13 @@ class TALGenerator:
for item in attrlist: for item in attrlist:
key = item[0] key = item[0]
if repldict.has_key(key): if repldict.has_key(key):
expr, xlat = repldict[key] expr, xlat, msgid = repldict[key]
item = item[:2] + ("replace", expr, xlat) item = item[:2] + ("replace", expr, xlat, msgid)
del repldict[key] del repldict[key]
newlist.append(item) newlist.append(item)
# Add dynamic-only attributes # Add dynamic-only attributes
for key, (expr, xlat) in repldict.items(): for key, (expr, xlat, msgid) in repldict.items():
newlist.append((key, None, "insert", expr, xlat)) newlist.append((key, None, "insert", expr, xlat, msgid))
return newlist return newlist
def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict, def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
...@@ -651,16 +657,18 @@ class TALGenerator: ...@@ -651,16 +657,18 @@ class TALGenerator:
else: else:
repldict = {} repldict = {}
if i18nattrs: if i18nattrs:
i18nattrs = i18nattrs.split() i18nattrs = _parseI18nAttributes(i18nattrs, self.position,
self.xml)
else: else:
i18nattrs = () i18nattrs = {}
# Convert repldict's name-->expr mapping to a # Convert repldict's name-->expr mapping to a
# name-->(compiled_expr, translate) mapping # name-->(compiled_expr, translate) mapping
for key, value in repldict.items(): 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: for key in i18nattrs:
if not repldict.has_key(key): if not repldict.has_key(key):
repldict[key] = None, 1 repldict[key] = None, 1, i18nattrs.get(key)
else: else:
repldict = {} repldict = {}
if replace: if replace:
...@@ -782,6 +790,30 @@ class TALGenerator: ...@@ -782,6 +790,30 @@ class TALGenerator:
if defineMacro: if defineMacro:
self.emitDefineMacro(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(): def test():
t = TALGenerator() t = TALGenerator()
t.pushProgram() t.pushProgram()
......
...@@ -349,7 +349,7 @@ class TALInterpreter: ...@@ -349,7 +349,7 @@ class TALInterpreter:
return self.attrAction(item) return self.attrAction(item)
name, value, action = item[:3] name, value, action = item[:3]
ok = 1 ok = 1
expr, msgid = item[3:] expr, xlat, msgid = item[3:]
if self.html and name.lower() in BOOLEAN_HTML_ATTRS: if self.html and name.lower() in BOOLEAN_HTML_ATTRS:
evalue = self.engine.evaluateBoolean(item[3]) evalue = self.engine.evaluateBoolean(item[3])
if evalue is self.Default: if evalue is self.Default:
...@@ -368,23 +368,18 @@ class TALInterpreter: ...@@ -368,23 +368,18 @@ class TALInterpreter:
if evalue is None: if evalue is None:
ok = 0 ok = 0
value = evalue value = evalue
if msgid:
value = self.i18n_attribute(value) if ok:
if value is None: if xlat:
value = name translated = self.translate(msgid or value, value, {})
value = '%s="%s"' % (name, escape(value, 1)) if translated is not None:
value = translated
if value is None:
value = name
value = '%s="%s"' % (name, escape(value, 1))
return ok, name, value return ok, name, value
bytecode_handlers["<attrAction>"] = attrAction 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): def no_tag(self, start, program):
state = self.saveState() state = self.saveState()
self.stream = stream = self.StringIO() self.stream = stream = self.StringIO()
...@@ -553,36 +548,19 @@ class TALInterpreter: ...@@ -553,36 +548,19 @@ class TALInterpreter:
# subnodes, which should /not/ go to the output stream. # subnodes, which should /not/ go to the output stream.
tmpstream = self.StringIO() tmpstream = self.StringIO()
self.interpretWithStream(stuff[1], tmpstream) self.interpretWithStream(stuff[1], tmpstream)
content = None default = tmpstream.getvalue()
# We only care about the evaluated contents if we need an implicit # We only care about the evaluated contents if we need an implicit
# message id. All other useful information will be in the i18ndict on # message id. All other useful information will be in the i18ndict on
# the top of the i18nStack. # the top of the i18nStack.
if msgid == '': if msgid == '':
content = tmpstream.getvalue() msgid = normalize(default)
msgid = normalize(content)
self.i18nStack.pop() self.i18nStack.pop()
# See if there is was an i18n:data for msgid # See if there is was an i18n:data for msgid
if len(stuff) > 2: if len(stuff) > 2:
obj = self.engine.evaluate(stuff[2]) obj = self.engine.evaluate(stuff[2])
xlated_msgid = self.translate(msgid, i18ndict, obj) xlated_msgid = self.translate(msgid, default, i18ndict, obj)
# If there is no translation available, use evaluated content. assert xlated_msgid is not None, self.position
if xlated_msgid is None: self._stream_write(xlated_msgid)
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)
bytecode_handlers['insertTranslation'] = do_insertTranslation bytecode_handlers['insertTranslation'] = do_insertTranslation
def do_insertStructure(self, stuff): def do_insertStructure(self, stuff):
...@@ -636,23 +614,14 @@ class TALInterpreter: ...@@ -636,23 +614,14 @@ class TALInterpreter:
self.interpret(block) self.interpret(block)
bytecode_handlers["loop"] = do_loop bytecode_handlers["loop"] = do_loop
def translate(self, msgid, i18ndict=None, obj=None): def translate(self, msgid, default, i18ndict, obj=None):
# XXX is this right?
if i18ndict is None:
i18ndict = {}
if obj: if obj:
i18ndict.update(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: if not self.i18nInterpolate:
return msgid 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 # 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)): def do_rawtextColumn(self, (s, col)):
self._stream_write(s) self._stream_write(s)
......
...@@ -53,7 +53,7 @@ FILE = "tests/input/test01.xml" ...@@ -53,7 +53,7 @@ FILE = "tests/input/test01.xml"
class TestTranslations(DummyTranslationService): class TestTranslations(DummyTranslationService):
def translate(self, domain, msgid, mapping=None, context=None, def translate(self, domain, msgid, mapping=None, context=None,
target_language=None): target_language=None, default=None):
if msgid == 'timefmt': if msgid == 'timefmt':
return '%(minutes)s minutes after %(hours)s %(ampm)s' % mapping return '%(minutes)s minutes after %(hours)s %(ampm)s' % mapping
elif msgid == 'jobnum': elif msgid == 'jobnum':
...@@ -67,7 +67,8 @@ class TestTranslations(DummyTranslationService): ...@@ -67,7 +67,8 @@ class TestTranslations(DummyTranslationService):
return '%(name)s was born in %(country)s' % mapping return '%(name)s was born in %(country)s' % mapping
return DummyTranslationService.translate(self, domain, msgid, return DummyTranslationService.translate(self, domain, msgid,
mapping, context, mapping, context,
target_language) target_language,
default=default)
class TestEngine(DummyEngine): class TestEngine(DummyEngine):
def __init__(self, macros=None): 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): ...@@ -400,11 +400,11 @@ class TALGeneratorTestCases(TestCaseBase):
{'tal:attributes': 'href string:http://www.zope.org; x string:y', {'tal:attributes': 'href string:http://www.zope.org; x string:y',
'name': 'bar', 'href': 'foo'}), 'name': 'bar', 'href': 'foo'}),
('startTag', ('a', ('startTag', ('a',
[('href', 'foo', 'replace', '$string:http://www.zope.org$', 0), [('href', 'foo', 'replace', '$string:http://www.zope.org$', 0, None),
('name', 'name="bar"'), ('name', 'name="bar"'),
('tal:attributes', ('tal:attributes',
'href string:http://www.zope.org; x string:y', 'tal'), 'href string:http://www.zope.org; x string:y', 'tal'),
('x', None, 'insert', '$string:y$', 0)])), ('x', None, 'insert', '$string:y$', 0, None)])),
('endScope', ()), ('endScope', ()),
rawtext('link</a>'), rawtext('link</a>'),
]) ])
...@@ -418,7 +418,7 @@ class TALGeneratorTestCases(TestCaseBase): ...@@ -418,7 +418,7 @@ class TALGeneratorTestCases(TestCaseBase):
'tal:replace': 'structure string:<img>'}), 'tal:replace': 'structure string:<img>'}),
('insertStructure', ('insertStructure',
('$string:<img>$', ('$string:<img>$',
{'src': ('$string:foo.png$', 0)}, {'src': ('$string:foo.png$', 0, None)},
[('startTag', ('p', [('startTag', ('p',
[('tal:replace', 'structure string:<img>', 'tal'), [('tal:replace', 'structure string:<img>', 'tal'),
('tal:attributes', 'src string:foo.png', ('tal:attributes', 'src string:foo.png',
...@@ -496,10 +496,27 @@ class TALGeneratorTestCases(TestCaseBase): ...@@ -496,10 +496,27 @@ class TALGeneratorTestCases(TestCaseBase):
('setPosition', (1, 0)), ('setPosition', (1, 0)),
('beginScope', {'alt': 'foo', 'i18n:attributes': 'alt'}), ('beginScope', {'alt': 'foo', 'i18n:attributes': 'alt'}),
('startTag', ('img', ('startTag', ('img',
[('alt', 'foo', 'replace', None, 1), [('alt', 'foo', 'replace', None, 1, None),
('i18n:attributes', 'alt', 'i18n')])), ('i18n:attributes', 'alt', 'i18n')])),
('endScope', ()), ('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): def check_i18n_translate(self):
# input/test19.html # 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